├── .gitignore ├── Tests ├── LinuxMain.swift └── PerfectLDAPTests │ └── PerfectLDAPTests.swift ├── Package.swift ├── Sources └── PerfectLDAP │ ├── Utilities.swift │ └── PerfectLDAP.swift ├── README.zh_CN.md ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /*.resolved 6 | /*.pins 7 | /*.sh 8 | /*.ldif -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PerfectLDAPTests 3 | 4 | XCTMain([ 5 | testCase(PerfectLDAPTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PerfectLDAP", 8 | products: [ 9 | .library( 10 | name: "PerfectLDAP", 11 | targets: ["PerfectLDAP"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/PerfectSideRepos/Perfect-ICONV.git", from: "3.0.0"), 15 | .package(url: "https://github.com/PerfectlySoft/Perfect-libSASL.git", from: "1.0.0"), 16 | .package(url: "https://github.com/PerfectlySoft/Perfect-OpenLDAP.git", from: "1.0.0"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "PerfectLDAP", 21 | dependencies: ["PerfectICONV"]), 22 | .testTarget( 23 | name: "PerfectLDAPTests", 24 | dependencies: ["PerfectLDAP"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/PerfectLDAP/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // PerfectLDAP 4 | // 5 | // Created by Rocky Wei on 2017-01-21. 6 | // Copyright (C) 2017 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2017 - 2018 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import PerfectICONV 21 | import OpenLDAP 22 | /// C library of SASL 23 | import SASL 24 | 25 | extension Iconv { 26 | 27 | /// directly convert a string from a berval structure 28 | /// - parameters: 29 | /// - from: struct berval, pointer to transit 30 | /// - returns: 31 | /// encoded string 32 | public func convert(from: berval) -> String { 33 | let (ptr, _) = self.convert(buf: from.bv_val, length: Int(from.bv_len)) 34 | guard let p = ptr else { 35 | return "" 36 | }//end guard 37 | let str = String(validatingUTF8: p) 38 | p.deallocate() 39 | return str ?? "" 40 | }//end convert 41 | } 42 | 43 | extension Array { 44 | 45 | /// generic function of converting array to a null terminated pointer array 46 | /// *CAUTION* memory MUST BE RELEASED MANUALLY 47 | /// - return: 48 | /// a pointer array with each pointer is pointing the corresponding element, ending with a null pointer. 49 | public func asUnsafeNullTerminatedPointers() -> UnsafeMutablePointer?> { 50 | let pointers = UnsafeMutablePointer?>.allocate(capacity: self.count + 1) 51 | for i in 0 ..< self.count { 52 | let p = UnsafeMutablePointer.allocate(capacity: 1) 53 | p.initialize(to: self[i]) 54 | pointers.advanced(by: i).pointee = p 55 | } 56 | pointers.advanced(by: self.count).pointee = nil 57 | return pointers 58 | }//func 59 | }//end array 60 | 61 | public func withCArrayOfString(array: [String] = [], _ body: (UnsafeMutablePointer?>?) throws -> R) rethrows -> R { 62 | 63 | if array.isEmpty { 64 | return try body(nil) 65 | }//end if 66 | 67 | // duplicate the array and append a null string 68 | var attr: [String?] = array 69 | attr.append(nil) 70 | 71 | // duplicate again and turn it into an array of pointers 72 | var parr = attr.map { $0 == nil ? nil : ber_strdup($0!) } 73 | 74 | // perform the operation 75 | let r = try parr.withUnsafeMutableBufferPointer { try body ($0.baseAddress) } 76 | 77 | // release allocated string pointers. 78 | for p in parr { ber_memfree(UnsafeMutablePointer(mutating: p)) } 79 | 80 | return r 81 | }//end withCArrayOfString 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Tests/PerfectLDAPTests/PerfectLDAPTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PerfectLDAP 3 | import Foundation 4 | import PerfectICONV 5 | import OpenLDAP 6 | 7 | /* 8 | Note of setup testing environments: 9 | * Windows 2000 Advanced Server: Domain Controller with DNS Server, 10 | Only Support Simple Login, PORT 389 11 | * Windows 2003 / 2008 Server: Support FULL functions, with SSL PORT 636 12 | Active Directory Domain Services 13 | Active Directory Lightweight Directory Services 14 | DNS Server 15 | Identity Management for UNIX 16 | * Windows 2008 SSL Configuration: Choose Certificate Template of Kerberos V and 17 | * Then duplicate into LDAPoverSSL, import it to Local Computer Service Account. 18 | * as described in 19 | * https://social.technet.microsoft.com/wiki/contents/articles/2980.ldap-over-ssl-ldaps-certificate.aspx 20 | * mac: /etc/openldap/ldap.conf / linux: /etc/ldap/ldap.conf 21 | * MUST ADD: `TLS_REQCERT allow` 22 | */ 23 | /* 24 | Note for using docker as the testing server 25 | $ docker pull dinkel/openldap 26 | $ docker run -d -p 389:389 -e SLAPD_PASSWORD=password -e SLAPD_DOMAIN=example.com dinkel/openldap 27 | when finishing, try docker ps & kill 28 | */ 29 | class PerfectLDAPTests: XCTestCase { 30 | let testURL = "ldap://localhost" 31 | let testBDN = "cn=admin,dc=example,dc=com" 32 | let testPWD = "password" 33 | let testBAS = "cn=admin,dc=example,dc=com" 34 | let testCPG: Iconv.CodePage = .UTF8 35 | 36 | let testGSSAPI_URL = "ldap://localhost" 37 | let testGSSAPI_BAS = "cn=admin,dc=example,dc=com" 38 | let testGSSAPI_USR = "admin" 39 | 40 | let debug = true 41 | 42 | static var allTests : [(String, (PerfectLDAPTests) -> () throws -> Void)] { 43 | return [ 44 | ("testLoginGSSAPI", testLoginGSSAPI), 45 | ("testLoginPass", testLoginPass), 46 | ("testSearch", testSearch), 47 | ("testAttributeMod", testAttributeMod), 48 | //("testServerSort", testServerSort), // Server Sort is a very bad idea! 49 | ("testUserAndGroupManagement", testUserAndGroupManagement) 50 | ] 51 | } 52 | 53 | func testLoginGSSAPI() { 54 | // .GSSAPI has been tested on ipa.demo1.freeipa.org 55 | // but unfortunately the account expired. 56 | let cred = LDAP.Login(user: testGSSAPI_USR, mechanism: .SIMPLE) 57 | do { 58 | let ldap = try LDAP(url: testGSSAPI_URL, loginData: cred) 59 | let res = try ldap.search(base: testGSSAPI_BAS, scope:.SUBTREE) 60 | print(res) 61 | }catch(let err) { 62 | XCTFail("testLogin error: \(err)") 63 | } 64 | }//end testLoginFailed 65 | 66 | func testLoginPass() { 67 | let cred = LDAP.Login(binddn: testBDN, password: testPWD) 68 | do { 69 | let logsuc = expectation(description: "logsuc") 70 | let ldap = try LDAP(url: testURL) 71 | ldap.login(info: cred) { err in 72 | XCTAssertNil(err) 73 | logsuc.fulfill() 74 | print("log real passed") 75 | }//end log 76 | waitForExpectations(timeout: 10) { error in 77 | XCTAssertNil(error) 78 | }//end wait 79 | }catch(let err) { 80 | XCTFail("testLogin error: \(err)") 81 | } 82 | }//end testLogin 83 | 84 | func testUserAndGroupManagement() { 85 | let cred = LDAP.Login(binddn: testBDN, password: testPWD) 86 | do { 87 | let ldap = try LDAP(url: testURL, loginData:cred, codePage: testCPG) 88 | 89 | let man = expectation(description: "groupAdd") 90 | let groupName = "cn=snoozers,dc=example,dc=com" 91 | ldap.add(distinguishedName: groupName, 92 | attributes: ["objectClass":["top", "posixGroup"], 93 | "gidNumber":["123"]] ) { 94 | res in 95 | XCTAssertNil(res) 96 | man.fulfill() 97 | }//end search 98 | 99 | waitForExpectations(timeout: 10) { error in 100 | XCTAssertNil(error) 101 | } 102 | 103 | let deman = expectation(description: "groupRemove") 104 | ldap.delete(distinguishedName: groupName) { 105 | res in 106 | XCTAssertNil(res) 107 | deman.fulfill() 108 | }//end search 109 | waitForExpectations(timeout: 10) { error in 110 | XCTAssertNil(error) 111 | } 112 | }catch(let err) { 113 | XCTFail("error: \(err)") 114 | } 115 | } 116 | 117 | func testSearch () { 118 | let cred = LDAP.Login(binddn: testBDN, password: testPWD) 119 | do { 120 | let ldap = try LDAP(url: testURL, loginData:cred, codePage: testCPG) 121 | 122 | let ser = expectation(description: "search") 123 | ldap.search(base:testBAS, scope:.SUBTREE) { res in 124 | print(res) 125 | ser.fulfill() 126 | }//end search 127 | 128 | waitForExpectations(timeout: 10) { error in 129 | XCTAssertNil(error) 130 | } 131 | }catch(let err) { 132 | XCTFail("error: \(err)") 133 | } 134 | } 135 | /* Server Sort is a very very bad idea. Drop it. 136 | func testServerSort () { 137 | let cred = LDAP.Login(binddn: testBDN, password: testPWD) 138 | do { 139 | let ldap = try LDAP(url: testURL, loginData:cred, codePage: testCPG) 140 | print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") 141 | let sort = LDAP.sortingString(sortedBy: [("description", .ASC)]) 142 | print(sort) 143 | let res = try ldap.search(base:testBAS,scope:.SUBTREE, attributes: ["displayName", "description"], sortedBy: sort) 144 | print(res) 145 | }catch(let err) { 146 | XCTFail("server control: \(err)") 147 | } 148 | 149 | } 150 | */ 151 | func testAttributeMod () { 152 | let cred = LDAP.Login(binddn: testBDN, password: testPWD) 153 | do { 154 | let ldap = try LDAP(url: testURL, loginData:cred, codePage: testCPG) 155 | let rs = try ldap.search(base:testBDN, scope:.SUBTREE) 156 | print("=======================================================") 157 | print(rs) 158 | print("=======================================================") 159 | let mod = expectation(description: "modify") 160 | ldap.modify(distinguishedName: testBDN, attributes: ["description":["My New Title"]]) { err in 161 | mod.fulfill() 162 | XCTAssertNil(err) 163 | }//end add 164 | self.waitForExpectations(timeout: 10){ error in 165 | XCTAssertNil(error) 166 | } 167 | let res = try ldap.search(base:testBDN, scope:.SUBTREE) 168 | print("=======================================================") 169 | print(res) 170 | print("=======================================================") 171 | }catch(let err) { 172 | XCTFail("error: \(err)") 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Perfect-LDAP [English](README.md) 2 | 3 |

4 | 5 | Get Involed with Perfect! 6 | 7 |

8 | 9 |

10 | 11 | Star Perfect On Github 12 | 13 | 14 | Chat on Gitter 15 | 16 | 17 | Follow Perfect on Twitter 18 | 19 | 20 | Join the Perfect Slack 21 | 22 |

23 | 24 |

25 | 26 | Swift 4.0 27 | 28 | 29 | Platforms OS X | Linux 30 | 31 | 32 | License Apache 33 | 34 | 35 | PerfectlySoft Twitter 36 | 37 | 38 | Join the chat at https://gitter.im/PerfectlySoft/Perfect 39 | 40 | 41 | Slack Status 42 | 43 |

44 | 45 | 46 | 该项目实现了一个对 OpenLDAP 的封装函数库,用于访问 OpenLDAP 和 Windows Active Directory 服务器。 47 | 48 | 该软件使用SPM进行编译和测试,本软件也是[Perfect](https://github.com/PerfectlySoft/Perfect)项目的一部分。 49 | 50 | 请确保您已经安装并激活了最新版本的 Swift 4.0 tool chain 工具链。 51 | 52 | *注意*: 由于LDAP在很多操作系统上都存在不同的服务器实现,因此本文中凡是标明了 (⚠️试验性质⚠️) 的方法,都以为着可能并不适用于某种场合。但是作为开源软件函数库,您可以随时根据需要修改源代码以达到目标要求。 53 | 54 | ## 快速上手 55 | 56 | 首先请在您的项目 Package.swift 文件中增加依存关系: 57 | 58 | ``` swift 59 | .package(url: "https://github.com/PerfectlySoft/Perfect-LDAP.git", 60 | from: "3.0.0") 61 | 62 | // 在目标段需要注明 63 | depedencies: ["PerfectLDAP"] 64 | ``` 65 | 66 | 然后在源代码中导入 PerfectLDAP 函数库: 67 | 68 | ``` swift 69 | import PerfectLDAP 70 | ``` 71 | 72 | ## 连接到 LDAP 服务器 73 | 74 | 在连接到服务器时,您可以选择署名连接或者匿名连接。署名连接意味着您需要提供用户名密码之类的登陆信息,而匿名连接则不需要这些登录信息。完整的函数形式为: `LDAP(url:String, loginData: Login?, codePage: Iconv.CodePage)`. 其中 `codePage` 选项是为某些采用非UTF8编码方式的服务器而设置的。比如,如果使用`codePage: .GB2312`则可以连接到以简体中文为主要数据库编码的 LDAP 服务器。 75 | 76 | ### TLS 加密选项 77 | 78 | PerfectLDAP 提供 TLS 加密选项,能够提高访问安全性。换句话说,连接的URL地址可以是 `ldap://` 或者 `ldaps://`,如下所示: 79 | 80 | ``` swift 81 | // 连接到389端口,无编码加密 82 | let ld = try LDAP(url: "ldap://perfect.com") 83 | ``` 84 | 或者 85 | 86 | ``` swift 87 | // 连接到636端口,证书加密 88 | let ld = try LDAP(url: "ldaps://perfect.com") 89 | ``` 90 | 91 | ### 连接超时 92 | 93 | 连接完成后,LDAP 对象可以设置超时属性,单位是秒: 94 | 95 | ``` swift 96 | // 设置超时限制,以下例子将连接会话限制在10秒之内: 97 | connection.timeout = 10 98 | ``` 99 | 100 | ### 分阶段登录 101 | 102 | 许多服务器都要求执行操作前必须登录,但 PerfectLDAP 允许不同的登录方式,如下所示: 103 | 104 | ``` swift 105 | // 以下代码展示了连接时同步登录。 106 | // 注意当前线程会被锁住直到服务器返回或者超时。 107 | // 首先准备登录信息: 108 | let credential = LDAP.login( ... ) 109 | let connection = try LDAP(url: "ldaps://...", loginData: login) 110 | ``` 111 | 除了以上同步连接暨登录方法之外,您还可以选择先连接后登录,实现多线程异步操作: 112 | 113 | ``` swift 114 | // 首先创建连接 115 | let connection = try LDAP(url: "ldaps:// ...") 116 | 117 | // 准备登录信息 118 | let credential = LDAP.login( ... ) 119 | 120 | // 采用异步方式登录 121 | connection.login(info: credential) { err in 122 | // 注意正常情况下 err 是 nil ,如果 err 非空,则说明登录失败 123 | } 124 | ``` 125 | 126 | ## 登录方式选择 127 | 128 | PerfectLDAP 使用一个`LDAP.Login`对象来实现不同的登录选项。各个不同的安全方式区别在于该对象的构造函数。 129 | 130 | ### 用户名密码登录 131 | 132 | 最简单的方法就是用用户名(DN)密码进行登录,调用构造函数 `LDAP.login(binddn: String, password: String)`即可,如下所示: 133 | 134 | ``` swift 135 | let credential = LDAP.Login(binddn: "CN=judy,CN=Users,DC=perfect,DC=com", password: "0penLDAP") 136 | ``` 137 | ### GSSAPI 138 | 139 | 如果需要使用GSSAPI进行身份验证,请调用`LDAP.login(user:String, mechanism: AuthType)`函数构造登录信息(前提是用户已经提前取得有效票据): 140 | 141 | ``` swift 142 | // 下列操作可以生成一个GSSAPI票据 143 | let credential = LDAP.login(user: "judy", mechanism: .GSSAPI) 144 | ``` 145 | 146 | ### GSS-SPNEGO 和 Digest-MD5 (⚠️试验性质⚠️) 147 | 148 | 对于其他 SASL 交互式登录机制,比如GSS-SPNEGO 和 Digest-MD5,请调用构造函数`LDAP.login(authname: String, user: String, password: String, realm: String, mechanism: AuthType)` 获取登录配置: 149 | 150 | ``` swift 151 | // 将登录信息配置为DIGEST-MD5 152 | let credential = LDAP.Login(authname: "judy", user: "DN:CN=judy,CN=Users,DC=perfect,DC=com", password: "0penLDAP", realm: "PERFECT.COM", mechanism: .DIGEST) 153 | ``` 154 | *⚠️注意⚠️* 参数 `authname` 等价于 `SASL_CB_AUTHNAME`,而 `user` 对应 `SASL_CB_USER`名称。如果您的程序不需要其中的某些参数,只要将该参数设置为空(“”)即可忽略。 155 | 156 | ## 检索 157 | 158 | PerfectLDAP 提供了同步检索和异步检索两种方式,但函数参数都是一样的。 159 | 160 | ### 同步检索 161 | 162 | 同步检索将阻塞当前进程直到服务器返回或超时。完整的函数调用是`LDAP.search(base:String, filter:String, scope:Scope, attributes: [String], sortedBy: String) throws -> [String:[String:Any]]`,举例如下: 163 | 164 | ``` swift 165 | // 执行同步检索并返回完整的属性集合,采用自然方式排序;返回结果为字典类型 166 | let res = try connection.search(base: "CN=Users,DC=perfect,DC=com", filter:"(objectclass=*)") 167 | 168 | print(res) 169 | ``` 170 | 171 | ### 异步检索 172 | 173 | 异步检索将在单独的线程执行。线程结束时会调用回调函数,并将返回的查询结果传递给回调函数。完整的调用为 `LDAP.search(base:String, filter:String, scope:Scope, attributes: [String], sortedBy: String, completion: @escaping ([String:[String:Any]])-> Void)`。和前面的例子等价的异步操作为: 174 | 175 | ``` swift 176 | // 执行 177 | // 执行异步检索。结束时回调并返回完整的属性集合,采用自然方式排序;返回结果为字典类型 178 | connection.search(base: "CN=Users,DC=perfect,DC=com", filter:"(objectclass=*)") { 179 | res in 180 | print(res) 181 | } 182 | ``` 183 | 184 | ### 检索参数 185 | - base: String, 检索时采用的基本DN,默认为空 186 | - filter: String, 检索过滤条件,默认为 `"(objectclass=*)"`,即所有对象类 187 | - scope: Scope, 检索范围,即 .BASE(基本) .SINGLE_LEVEL(单层) .SUBTREE(子树) 或 .CHILDREN(所有分支) 188 | - sortedBy: String, 用于排序的字符串;可以用 `LDAP.sortingString()` 函数构造。 189 | - completion: 回调函数,其返回参数为一个字典。如果字典为空则意味着查询失败。 190 | 191 | #### 服务器端排序 (⚠️试验性质⚠️) 192 | 其中,`sortedBy` 参数是一个字符串,通知远程服务器按照该字符串的内容进行排序。但是您可以选择用下列方法构造该字符串,即用一个元组数组表示希望用一系列字段及其排序方法进行排序: 193 | 194 | ``` swift 195 | // 每个元组包括两个部分:排序字段以及排序方法 - .ASC 升序或 .DSC 降序 196 | let sort = LDAP.sortingString(sortedBy: [("description", .ASC)]) 197 | ``` 198 | 199 | ### 限制查询结果集 200 | 201 | 连接后,LDAP可以为查询结果设置限制选项`LDAP.limitation`。该整型变量用于说明每一个检索操作所能返回的最多记录数。 202 | 203 | ``` swift 204 | // 设置检索返回结果集。本例中,每个检索只能返回前一千个记录 205 | connection.limitation = 1000 206 | ``` 207 | 208 | ## 属性操作 209 | 210 | PerfectLDAP 为属性操作提供了增删改功能。每个操作都有同步和异步选项。 211 | 212 | ### 增加属性 (⚠️试验性质⚠️) 213 | 214 | 函数 `LDAP.add()` 可以针对一个特定 DN 增加属性,参数如下: 215 | - distinguishedName: String, 目标 DN (唯一命名) 216 | - attributes:[String:[String]], 以字典方式表达的属性集合。该字典中,每一个属性对应一个条目,每个条目之下都允许一个数组来保存多个字符串值。 217 | 218 | 无论同步add()还是异步add()都采用上面的参数,比如: 219 | 220 | ``` swift 221 | // 同步增加属性 222 | do { 223 | try connection.add(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["mail":["judy@perfect.com", "judy@perfect.org"]]) 224 | }catch (let err) { 225 | // 增加属性失败 226 | } 227 | 228 | // 异步增加属性 229 | connection.add(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["mail":["judy@perfect.com", "judy@perfect.org"]]) { err in 230 | // 如果成功,err 应该是 nil 231 | } 232 | ``` 233 | 234 | ### 修改属性 235 | 236 | 函数 `LDAP.modify()` 可以针对一个特定 DN 修改属性,参数如下: 237 | - distinguishedName: String, 目标 DN (唯一命名) 238 | - attributes:[String:[String]], 以字典方式表达的属性集合。该字典中,每一个属性对应一个条目,每个条目之下都允许一个数组来保存多个字符串值。 239 | 240 | 无论同步修改还是异步修改都采用上面的参数,比如: 241 | 242 | ``` swift 243 | // 同步修改属性 244 | do { 245 | try connection.modify(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["codePage":["437"]]) 246 | }catch (let err) { 247 | // 修改属性失败 248 | } 249 | 250 | // 异步修改属性 251 | connection.modify(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes:["codePage":["437"]]) { err in 252 | // 如果成功,err 应该是 nil 253 | } 254 | ``` 255 | 256 | ### 删除属性(⚠️试验性质⚠️) 257 | 258 | 函数 `LDAP.delete()` 用于删除特定DN的所有属性,只有一个参数: 259 | - distinguishedName: String, 目标 DN (唯一命名) 260 | 261 | 无论同步删除还是异步删除都采用上面的参数,比如: 262 | 263 | ``` swift 264 | // 同步删除属性 265 | do { 266 | try connection.delete(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com") 267 | }catch (let err) { 268 | // 删除属性失败 269 | } 270 | 271 | // 异步删除属性 272 | connection.delete(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com") { err in 273 | // 如果成功,err 应该是 nil 274 | } 275 | ``` 276 | 277 | 278 | ### 问题报告、内容贡献和客户支持 279 | 280 | 我们目前正在过渡到使用JIRA来处理所有源代码资源合并申请、修复漏洞以及其它有关问题。因此,GitHub 的“issues”问题报告功能已经被禁用了。 281 | 282 | 如果您发现了问题,或者希望为改进本文提供意见和建议,[请在这里指出](http://jira.perfect.org:8080/servicedesk/customer/portal/1). 283 | 284 | 在您开始之前,请参阅[目前待解决的问题清单](http://jira.perfect.org:8080/projects/ISS/issues). 285 | 286 | ## 更多信息 287 | 关于本项目更多内容,请参考[perfect.org](http://perfect.org). 288 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perfect-LDAP [简体中文](README.zh_CN.md) 2 | 3 |

4 | 5 | Get Involed with Perfect! 6 | 7 |

8 | 9 |

10 | 11 | Star Perfect On Github 12 | 13 | 14 | Stack Overflow 15 | 16 | 17 | Follow Perfect on Twitter 18 | 19 | 20 | Join the Perfect Slack 21 | 22 |

23 | 24 |

25 | 26 | Swift 4.0 27 | 28 | 29 | Platforms OS X | Linux 30 | 31 | 32 | License Apache 33 | 34 | 35 | PerfectlySoft Twitter 36 | 37 | 38 | Slack Status 39 | 40 |

41 | 42 | This project provides an express OpenLDAP class wrapper which enable access to OpenLDAP servers and Windows Active Directory server. 43 | 44 | This package builds with Swift Package Manager and is part of the [Perfect](https://github.com/PerfectlySoft/Perfect) project. 45 | 46 | Ensure you have installed and activated the latest Swift 4.0 tool chain. 47 | 48 | *Caution*: for the reason that LDAP is widely using in many different operating systems with variable implementations, API marked with (⚠️EXPERIMENTAL⚠️) indicates that this method might not be fully applicable to certain context. However, as an open source software library, you may modify the source code to meet a specific requirement. 49 | 50 | ## Quick Start 51 | 52 | Add the following dependency to your project's Package.swift file: 53 | 54 | ``` swift 55 | .package(url: "https://github.com/PerfectlySoft/Perfect-LDAP.git", 56 | from: "3.0.0") 57 | 58 | // in the target section: 59 | depedencies: ["PerfectLDAP"] 60 | ``` 61 | 62 | Then import PerfectLDAP to your source code: 63 | 64 | 65 | ``` swift 66 | 67 | import PerfectLDAP 68 | 69 | ``` 70 | 71 | 72 | ## Connect to LDAP Server 73 | 74 | You can create actual connections as need with or without login credential. The full API is `LDAP(url:String, loginData: Login?, codePage: Iconv.CodePage)`. The `codePage` option is for those servers applying character set other than .UTF8, e.g., set `codePage: .GB2312` to connect to LDAP server in Simplified Chinese. 75 | 76 | ### TLS Option 77 | 78 | PerfectLDAP provides TLS options for network security considerations, i.e, you can choose either `ldap://` or `ldaps://` for connections, as demo below: 79 | 80 | ``` swift 81 | // this will connect to a 389 port without any encryption 82 | let ld = try LDAP(url: "ldap://perfect.com") 83 | ``` 84 | or, 85 | 86 | ``` swift 87 | // this will connect to a 636 port with certificates 88 | let ld = try LDAP(url: "ldaps://perfect.com") 89 | ``` 90 | 91 | ### Connection Timeout 92 | 93 | Once connected, LDAP object could be set with timeout option, and the timing unit is second: 94 | 95 | ``` swift 96 | // set the timeout for communication. In this example, connection will be timeout in ten seconds. 97 | connection.timeout = 10 98 | ``` 99 | 100 | ### Login or Anonymous 101 | 102 | Many servers mandate login before performing any actual LDAP operations, however, PerfectLDAP provides multiple login options as demo below: 103 | 104 | ``` swift 105 | // this snippet demonstrate how to connect to LDAP server with a login credential 106 | // NOTE: this kind of connection will block the thread until server return or timeout. 107 | // create login credential 108 | let credential = LDAP.login( ... ) 109 | let connection = try LDAP(url: "ldaps://...", loginData: login) 110 | ``` 111 | Aside the above synchronous login option, a two phased threading login process could also bring more controls to the application: 112 | 113 | ``` swift 114 | // first create a connection 115 | let connection = try LDAP(url: "ldaps:// ...") 116 | 117 | // setup login info 118 | let credential = LDAP.login( ... ) 119 | 120 | // login in a separated thread 121 | connection.login(info: credential) { err in 122 | // if err is not nil, then something must be wrong in the login process. 123 | } 124 | ``` 125 | 126 | ## Login Options 127 | 128 | PerfectLDAP provides a special object called `LDAP.Login` to store essential account information for LDAP connections and the form of constructor is subject to the authentication types: 129 | 130 | ### Simple Login 131 | 132 | To use simple login method, simply call `LDAP.login(binddn: String, password: String)`, as snippet below: 133 | 134 | ``` swift 135 | let credential = LDAP.Login(binddn: "CN=judy,CN=Users,DC=perfect,DC=com", password: "0penLDAP") 136 | ``` 137 | ### GSSAPI 138 | 139 | To apply GSSAPI authentication, call `LDAP.login(user:String, mechanism: AuthType)` to construct a login credential (assuming the user has already acquired a valid ticket): 140 | 141 | ``` swift 142 | // this call will generate a GSSAPI login credential 143 | let credential = LDAP.login(user: "judy", mechanism: .GSSAPI) 144 | ``` 145 | 146 | ### GSS-SPNEGO and Digest-MD5 (⚠️EXPERIMENTAL⚠️) 147 | 148 | To apply other SASL mechanisms, such as GSS-SPNEGO and Digest-MD5 interactive logins, call `LDAP.login(authname: String, user: String, password: String, realm: String, mechanism: AuthType)` as demo below: 149 | 150 | ``` swift 151 | // apply DIGEST-MD5 mechanism. 152 | let credential = LDAP.Login(authname: "judy", user: "DN:CN=judy,CN=Users,DC=perfect,DC=com", password: "0penLDAP", realm: "PERFECT.COM", mechanism: .DIGEST) 153 | ``` 154 | *⚠️NOTE⚠️* The `authname` is equivalent to `SASL_CB_AUTHNAME` and `user` is actually the macro of `SASL_CB_USER`. If any parameter above is not applicable to your case, simply assign an empty string "" to ignore it. 155 | 156 | ## Search 157 | 158 | PerfectLDAP provides asynchronous and synchronous version of searching API with the same parameters: 159 | 160 | ### Synchronous Search 161 | 162 | Synchronous search will block the thread until server returns, the full api is `LDAP.search(base:String, filter:String, scope:Scope, attributes: [String], sortedBy: String) throws -> [String:[String:Any]]`. Here is an example: 163 | 164 | ``` swift 165 | // perform an ldap search synchronously, which will return a full set of attributes 166 | // with a natural (unsorted) order, in form of a dictionary. 167 | let res = try connection.search(base: "CN=Users,DC=perfect,DC=com", filter:"(objectclass=*)") 168 | 169 | print(res) 170 | ``` 171 | 172 | ### Asynchronous Search 173 | 174 | Asynchronous search allows performing search in an independent thread. Once completed, the thread will call back with the result set in a dictionary. Full api of asynchronous search is `LDAP.search(base:String, filter:String, scope:Scope, attributes: [String], sortedBy: String, completion: @escaping ([String:[String:Any]])-> Void)`. The equivalent example is: 175 | 176 | ``` swift 177 | // perform an ldap search asynchronously, which will return a full set of attributes 178 | // with a natural (unsorted) order, in form of a dictionary. 179 | connection.search(base: "CN=Users,DC=perfect,DC=com", filter:"(objectclass=*)") { 180 | res in 181 | print(res) 182 | } 183 | ``` 184 | 185 | ### Parameters of Search 186 | - base: String, search base domain (dn), default = "" 187 | - filter: String, the filter of query, default is `"(objectclass=*)"`, means all possible results 188 | - scope: Searching Scope, i.e., .BASE, .SINGLE_LEVEL, .SUBTREE or .CHILDREN 189 | - sortedBy: a sorting string, may also be generated by `LDAP.sortingString()` 190 | - completion: callback with a parameter of dictionary, empty if failed 191 | 192 | #### Server Side Sort (⚠️EXPERIMENTAL⚠️) 193 | The `sortedBy` parameters is a string that indicates the remote server to perform search with a sorted set. PerfectLDAP provides a more verbal way to build such a string, i.e, an array of tuples to describe what attributes would control the result set: 194 | 195 | ``` swift 196 | // each tuple consists two parts: the sorting field and its order - .ASC or .DSC 197 | let sort = LDAP.sortingString(sortedBy: [("description", .ASC)]) 198 | ``` 199 | 200 | ### Limitation of Searching Result 201 | 202 | Once connected, LDAP object could be set with an limitation option - `LDAP.limitation`. It is an integer which specifies the maximum number of entries that can be returned on a search operation. 203 | 204 | ``` swift 205 | // set the limitation for searching result set. In this example, only the first 1000 entries will return. 206 | connection.limitation = 1000 207 | ``` 208 | 209 | ## Attribute Operations 210 | 211 | PerfectLDAP provides add() / modify() and delete() for attributes operations with both synchronous and asynchronous options. 212 | 213 | ### Add Attributes (⚠️EXPERIMENTAL⚠️) 214 | 215 | Function `LDAP.add()` can add attributes to a specific DN with parameters below: 216 | - distinguishedName: String, specific DN 217 | - attributes:[String:[String]], attributes as an dictionary to add. In this dictionary, every attribute, as a unique key in the dictionary, could have a series of values as an array. 218 | 219 | Both asynchronous add() and synchronous add() share the same parameters above, take example: 220 | 221 | ``` swift 222 | // try an add() synchronously. 223 | do { 224 | try connection.add(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["mail":["judy@perfect.com", "judy@perfect.org"]]) 225 | }catch (let err) { 226 | // failed for some reason 227 | } 228 | 229 | // try and add() asynchronously: 230 | connection.add(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["mail":["judy@perfect.com", "judy@perfect.org"]]) { err in 231 | // if nothing wrong, err will be nil 232 | } 233 | ``` 234 | 235 | ### Modify Attributes 236 | 237 | Function `LDAP.modify()` can modify attributes from a specific DN with parameters below: 238 | - distinguishedName: String, specific DN 239 | - attributes:[String:[String]], attributes as an dictionary to modify. In this dictionary, every attribute, as a unique key in the dictionary, could have a series of values as an array. 240 | - method: specify if an attribute should be added, removed or replaced (default) 241 | - add: LDAP_MOD_ADD | LDAP_MOD_BVALUES 242 | - remove: LDAP_MOD_DELETE | LDAP_MOD_BVALUES 243 | - replace: LDAP_MOD_REPLACE | LDAP_MOD_BVALUES 244 | 245 | Both asynchronous modify() and synchronous modify() share the same parameters above, take example: 246 | 247 | ``` swift 248 | // try and modify() synchronously. 249 | do { 250 | try connection.modify(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes: ["codePage":["437"]]) 251 | }catch (let err) { 252 | // failed for some reason 253 | } 254 | 255 | // try and modify() asynchronously: 256 | connection.modify(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com", attributes:["codePage":["437"]]) { err in 257 | // if nothing wrong, err will be nil 258 | } 259 | ``` 260 | 261 | Example: Add and remove user from group 262 | 263 | ``` swift 264 | // add user to group 265 | do { 266 | try connection.modify(distinguishedName: "CN=employee_group,CN=Group,DC=perfect,DC=com", attributes: ["member":["CN=judy,CN=User,DC=perfect,DC=com"]], method: LDAP_MOD_ADD | LDAP_MOD_BVALUES) 267 | }catch (let err) { 268 | // failed for some reason 269 | } 270 | 271 | // remove user from group 272 | do { 273 | try connection.modify(distinguishedName: "CN=employee_group,CN=Group,DC=perfect,DC=com", attributes: ["member":["CN=judy,CN=User,DC=perfect,DC=com"]], method: LDAP_MOD_DELETE | LDAP_MOD_BVALUES) 274 | }catch (let err) { 275 | // failed for some reason 276 | } 277 | ``` 278 | 279 | ### Delete Attributes (⚠️EXPERIMENTAL⚠️) 280 | 281 | Function `LDAP.delete()` can delete attributes from a specific DN with only one parameter: 282 | - distinguishedName: String, specific DN 283 | 284 | Both asynchronous delete() and synchronous delete() share the same parameter above, take example: 285 | 286 | ``` swift 287 | // try an delete() synchronously. 288 | do { 289 | try connection.delete(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com") 290 | }catch (let err) { 291 | // failed for some reason 292 | } 293 | 294 | // try and delete() asynchronously: 295 | connection.delete(distinguishedName: "CN=judy,CN=User,DC=perfect,DC=com") { err in 296 | // if nothing wrong, err will be nil 297 | } 298 | ``` 299 | 300 | ## Further Information 301 | For more information on the Perfect project, please visit [perfect.org](http://perfect.org). 302 | -------------------------------------------------------------------------------- /Sources/PerfectLDAP/PerfectLDAP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectLDAP.swift 3 | // PerfectLDAP 4 | // 5 | // Created by Rocky Wei on 2017-01-17. 6 | // Copyright (C) 2017 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2017 - 2018 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | /// C library of SASL 21 | import SASL 22 | 23 | /// C library of OpenLDAP 24 | import OpenLDAP 25 | 26 | /// Iconv 27 | import PerfectICONV 28 | 29 | import Dispatch 30 | 31 | /// Perfect LDAP Module 32 | public class LDAP { 33 | 34 | /// Searching Scope 35 | public enum Scope : ber_int_t { 36 | case BASE = 0, SINGLE_LEVEL = 1, SUBTREE = 2, CHILDREN = 3, DEFAULT = -1 37 | }//end 38 | 39 | let threading = DispatchQueue(label: "ldap.thread.\(time(nil))") 40 | 41 | /// Authentication Model 42 | public enum AuthType:String { 43 | /// username@domain & password 44 | case SIMPLE = "" 45 | /// GSS-API 46 | case GSSAPI = "GSSAPI" 47 | /// GSS-SPNEGO 48 | case SPNEGO = "GSS-SPNEGO" 49 | /// DIGEST MD5 50 | case DIGEST = "DIGEST-MD5" 51 | /// OTHER 52 | case OTHER = "UNSUPPORTED" 53 | }//end 54 | 55 | 56 | /// Login Data 57 | public class Login { 58 | 59 | /// the name of SASL_CB_AUTHNAME, the username to authenticate, 60 | /// without any symbols or suffix, usually a lowercased short name "someone" 61 | public var authname = "" // "Someone" 62 | 63 | /// distinguished name, usually in form of "CN=Someone,CN=User,CN=domain,CN=com" 64 | public var binddn = "" 65 | 66 | /// the name of SASL_CB_USER, the username to use for proxy authorization 67 | /// usually with a prefix of "dn=" with binddn: "DN:CN=Someone,CN=User,CN=domain,CN=com" 68 | public var user = "" 69 | 70 | /// the password of login 71 | public var password = "" 72 | 73 | /// the name of SASL_CB_GETREALM, the realm for the authentication attempt 74 | public var realm = "" 75 | 76 | /// mechanism for authentication 77 | public var mechanism: AuthType = .SIMPLE 78 | 79 | /// garbage manager 80 | internal var trashcan: [UnsafeMutablePointer?] = [] 81 | 82 | public func drop(garbage: UnsafeMutablePointer?) { trashcan.append(garbage) } 83 | 84 | /// contructor for simple login 85 | /// - parameters: 86 | /// - binddn: distinguished name, usually in form of "CN=Someone,CN=User,CN=domain,CN=com" 87 | /// - password: the password 88 | public init(binddn: String = "", password: String = "") { 89 | self.binddn = binddn 90 | self.password = password 91 | self.mechanism = .SIMPLE 92 | }//end init 93 | 94 | /// constructor for SASL 95 | /// - parameters: 96 | /// - authname: the name of SASL_CB_AUTHNAME, the username to authenticate, without any symbols or suffix, usually a lowercased short name "someone" 97 | /// - user: the name of SASL_CB_USER, the username to use for proxy authorization, usually with a prefix of "dn=" with binddn: "DN:CN=Someone,CN=User,CN=domain,CN=com" 98 | /// - password: the password of login 99 | /// - realm: the name of SASL_CB_GETREALM, the realm for the authentication attempt 100 | /// - mechanism: the SASL mechanism 101 | public init(authname: String = "", user: String = "", password: String = "", realm: String = "", mechanism: AuthType = .GSSAPI) { 102 | self.binddn = "" 103 | self.authname = authname 104 | self.user = user 105 | self.password = password 106 | self.realm = realm 107 | self.mechanism = mechanism 108 | }//end init 109 | 110 | deinit { 111 | for garbage in trashcan { 112 | if garbage == nil { 113 | continue 114 | }//end if 115 | ber_memfree(garbage) 116 | }//next 117 | trashcan.removeAll() 118 | }//end 119 | }//end Login 120 | 121 | /// Error Handling 122 | public enum Exception: Error { 123 | /// Error with Message 124 | case message(String) 125 | }//end enum 126 | 127 | /// Explain the error code, typical usage is `throw Exception.message(LDAP.error(error_number))` 128 | /// - parameters: 129 | /// - errno: Int32, the error number return by most ldap_XXX functions 130 | /// - returns: 131 | /// a short text of explaination in English. *NOTE* string pointer of err2string is static so don't free it 132 | public static func error(_ errno: Int32) -> String { 133 | return String(cString: ldap_err2string(errno)) 134 | }//end error 135 | 136 | /// time out value in terms of querying process, in seconds 137 | public var timeout: Int { 138 | get { 139 | var t = timeval(tv_sec: 0, tv_usec: 0) 140 | let _ = ldap_get_option(ldap, LDAP_OPT_TIMEOUT, &t) 141 | return t.tv_sec 142 | }//end get 143 | set { 144 | var t = timeval(tv_sec: newValue, tv_usec: 0) 145 | let _ = ldap_set_option(ldap, LDAP_OPT_TIMEOUT, &t) 146 | }//end set 147 | }//end timetout 148 | 149 | /// the maximum number of entries that can be returned on a search operation 150 | public var limitation: Int { 151 | get { 152 | var limit = 0 153 | let _ = ldap_get_option(ldap, LDAP_OPT_SIZELIMIT, &limit) 154 | return limit 155 | }//end get 156 | set { 157 | var limit = newValue 158 | let _ = ldap_set_option(ldap, LDAP_OPT_SIZELIMIT, &limit) 159 | }//end set 160 | }//end limitation 161 | 162 | /// LDAP handler pointer 163 | internal var ldap: OpaquePointer? = nil 164 | 165 | /// codepage convertor 166 | internal var iconv: Iconv? = nil 167 | 168 | /// codepage reversely convertor 169 | internal var iconvR: Iconv? = nil 170 | 171 | /// convert string if encoding is required 172 | /// - parameters: 173 | /// - ber: struct berval of the original buffer 174 | /// - returns: 175 | /// encoded string 176 | public func string(ber: berval) -> String { 177 | guard let i = iconv else { 178 | return String(validatingUTF8: ber.bv_val) ?? "" 179 | }//end i 180 | return i.convert(from: ber) 181 | }//end string 182 | 183 | /// convert string if encoding is required 184 | /// - parameters: 185 | /// - pstr: pointer of the original buffer, will apply null-terminated method 186 | /// - returns: 187 | /// encoded string 188 | public func string(pstr: UnsafeMutablePointer) -> String { 189 | let ber = berval(bv_len: ber_len_t(strlen(pstr)), bv_val: pstr) 190 | return self.string(ber: ber) 191 | }//end ber 192 | 193 | /// convert string to encoded binary data reversely 194 | /// *NOTE* MUST BE FREE MANUALLY 195 | /// - parameters: 196 | /// - str: source utf8 string 197 | /// - returns: 198 | /// encoded berval structure 199 | public func string(str: String) -> berval { 200 | guard let i = iconvR else { 201 | return str.withCString { ptr -> berval in 202 | return berval(bv_len: ber_len_t(str.utf8.count), bv_val: ber_strdup(ptr)) 203 | }//end str 204 | }//end str 205 | return str.withCString { ptr -> berval in 206 | let (p, sz) = i.convert(buf: ptr, length: str.utf8.count) 207 | return berval(bv_len: ber_len_t(sz), bv_val: p) 208 | }//end str 209 | }//end string 210 | 211 | /// constructor of LDAP. could be a simple LDAP() to local server or LDAP(url) with / without logon options. 212 | /// if login parameters were input, the process would block until finished. 213 | /// so it is strongly recommanded that call LDAP() without login option and call ldap.login() {} in async mode 214 | /// - parameters: 215 | /// - url: String, something like ldap://somedomain.com:port or ldaps://somedomain.com 216 | /// - login: login data. 217 | /// - codePage: object server coding page, e.g., GB2312, BIG5 or JS 218 | /// - throws: 219 | /// possible exceptions of initial failed or access denied 220 | public init(url:String = "ldaps://localhost", loginData: Login? = nil, codePage: Iconv.CodePage = .UTF8) throws { 221 | 222 | if codePage != .UTF8 { 223 | // we need a pair of code pages to transit in both directions. 224 | iconv = try Iconv(from: codePage, to: .UTF8) 225 | iconvR = try Iconv(from: .UTF8, to: codePage) 226 | }//end if 227 | 228 | ldap = OpaquePointer(bitPattern: 0) 229 | var r = ldap_initialize(&ldap, url) 230 | 231 | guard r == 0 else { 232 | throw Exception.message(LDAP.error(r)) 233 | }//end guard 234 | 235 | var proto = LDAP_VERSION3 236 | 237 | r = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &proto) 238 | 239 | // if no login required, skip. 240 | if loginData == nil { 241 | return 242 | }//end if 243 | 244 | // call login internally 245 | try login(info: loginData) 246 | }//end init 247 | 248 | /// login in synchronized mode, will block the calling thread 249 | /// - parameters: 250 | /// - info: login data 251 | /// - throws: 252 | /// Exception message 253 | 254 | public func login(info: Login?) throws { 255 | 256 | // load login data 257 | guard let inf = info else { 258 | throw Exception.message("LOGIN DATA NOT AVAILABLE") 259 | }//end guard 260 | 261 | // prepare return value 262 | var r = Int32(0) 263 | 264 | // prepare an empty credential structure 265 | var cred = berval(bv_len: 0, bv_val: UnsafeMutablePointer(bitPattern: 0)) 266 | 267 | if inf.mechanism == .OTHER { 268 | throw Exception.message("UNSUPPORTED MECHANISMS") 269 | }//end if 270 | 271 | if inf.mechanism == .SIMPLE { 272 | // simple auth just use password and binddn to login 273 | cred.bv_val = ber_strdup(inf.password) 274 | cred.bv_len = ber_len_t(strlen(cred.bv_val)) 275 | r = ldap_sasl_bind_s(self.ldap, inf.binddn, nil, &cred, nil, nil, nil) 276 | ber_memfree(cred.bv_val) 277 | if r == 0 { 278 | return 279 | }else { 280 | throw Exception.message(LDAP.error(r)) 281 | }//end 282 | }//end if 283 | 284 | // turn the login info data into a pointer by this one. 285 | var pinf = inf 286 | 287 | // call the binding 288 | r = ldap_sasl_interactive_bind_s(self.ldap, inf.binddn, inf.mechanism.rawValue, nil, nil, LDAP_SASL_QUIET, { ld, flags, pRawDefaults, pRawInteract -> Int32 in 289 | 290 | // in this callback, convert the pointer of pointers back to defaults 291 | let pDef = unsafeBitCast(pRawDefaults, to: UnsafeMutablePointer.self) 292 | let pInt = unsafeBitCast(pRawInteract, to: UnsafeMutablePointer.self) 293 | let def = pDef.pointee 294 | var pcursor:UnsafeMutablePointer? = nil 295 | var interact: sasl_interact_t 296 | pcursor = pInt 297 | 298 | // loop & answer the question asked by server 299 | while(pcursor != nil) { 300 | interact = (pcursor?.pointee)! 301 | 302 | // prepare a blank pointer 303 | var dflt = "" 304 | switch(Int32(interact.id)) { 305 | case SASL_CB_AUTHNAME: 306 | dflt = def.authname 307 | case SASL_CB_USER: 308 | dflt = def.user 309 | case SASL_CB_PASS: 310 | dflt = def.password 311 | case SASL_CB_GETREALM: 312 | dflt = def.realm 313 | case SASL_CB_LIST_END: 314 | return 0 315 | case SASL_CB_NOECHOPROMPT, SASL_CB_ECHOPROMPT: 316 | dflt = "" 317 | default: 318 | return 0 319 | }//end case 320 | 321 | // once 322 | if !dflt.isEmpty { 323 | let str = ber_strdup(dflt) 324 | interact.result = unsafeBitCast(str, to: UnsafeRawPointer.self) 325 | interact.len = UInt32(dflt.utf8.count) 326 | def.drop(garbage: str) 327 | }else{ 328 | interact.len = 0 329 | }//end if 330 | pcursor?.pointee = interact 331 | pcursor = pcursor?.successor() 332 | }//end while 333 | return 0 334 | }, UnsafeMutableRawPointer(UnsafeMutablePointer(mutating: &pinf))) 335 | 336 | if r == 0 { 337 | return 338 | }else { 339 | throw Exception.message(LDAP.error(r)) 340 | }//end 341 | }//end login 342 | 343 | /// Login in asynchronized mode. Once completed, it would invoke the callback handler 344 | /// - parameters: 345 | /// - info: login data 346 | /// - completion: callback handler with a boolean parameter indicating whether login succeeded or not. 347 | public func login(info: Login, completion: @escaping (String?)->Void) { 348 | threading.async { 349 | do { 350 | try self.login(info: info) 351 | completion(nil) 352 | }catch(let err) { 353 | completion("LOGIN FAILED: \(err)") 354 | }//end do 355 | }//end thread 356 | }//end login 357 | 358 | /// destructor of the class 359 | deinit { 360 | ldap_unbind_ext_s(ldap, nil, nil) 361 | }//end deinit 362 | 363 | 364 | /// Attribute of a searching result 365 | internal struct Attribute { 366 | 367 | /// name of the attribute 368 | internal var _name = "" 369 | 370 | /// name of the attribute, read only 371 | public var name: String { get { return _name } } 372 | 373 | /// value set of the attribute, as an array of string 374 | internal var _values = [String]() 375 | 376 | /// value set of the attribute, as an array of string, read only 377 | public var values:[String] { get { return _values } } 378 | 379 | /// constructor of Attribute 380 | /// - parameters: 381 | /// - ldap: the LDAP handler 382 | /// - entry: the LDAPMessage (single element) 383 | /// - tag: attribute name returned by ldap_xxx_attribute 384 | public init (ldap: LDAP, entry:OpaquePointer, tag:UnsafePointer) { 385 | _name = String(cString: tag) 386 | let valueSet = ldap_get_values_len(ldap.ldap, entry, tag) 387 | var cursor = valueSet 388 | while(cursor != nil) { 389 | guard let pBer = cursor?.pointee else { 390 | break 391 | }//end guard 392 | let b = pBer.pointee 393 | _values.append(ldap.string(ber: b)) 394 | cursor = cursor?.successor() 395 | }//end cursor 396 | if valueSet != nil { 397 | ldap_value_free_len(valueSet) 398 | }//end if 399 | }//end init 400 | }//end Attribute 401 | 402 | /// Attributes Set of a Searching result 403 | internal struct AttributeSet { 404 | 405 | /// name of the attribute 406 | internal var _name = "" 407 | 408 | /// name of the attribute, read only 409 | public var name: String { get { return _name } } 410 | 411 | /// attribute value set array 412 | internal var _attributes = [Attribute]() 413 | 414 | /// attribute value set array, read only 415 | public var attributes: [Attribute] { get { return _attributes } } 416 | 417 | /// constructor of Attribute 418 | /// - parameters: 419 | /// - ldap: the LDAP handler 420 | /// - entry: the LDAPMessage (single element) 421 | public init (ldap: LDAP, entry:OpaquePointer) { 422 | guard let pName = ldap_get_dn(ldap.ldap, entry) else { 423 | return 424 | }//end pName 425 | _name = ldap.string(pstr: pName) 426 | ldap_memfree(pName) 427 | var ber = OpaquePointer(bitPattern: 0) 428 | var a = ldap_first_attribute(ldap.ldap, entry, &ber) 429 | while(a != nil) { 430 | _attributes.append(Attribute(ldap: ldap, entry: entry, tag: a!)) 431 | ldap_memfree(a) 432 | a = ldap_next_attribute(ldap.ldap, entry, ber) 433 | }//end while 434 | ber_free(ber, 0) 435 | }//end init 436 | }//end class 437 | 438 | /// a reference record of an LDAP search result 439 | internal struct Reference { 440 | 441 | /// value set in an array of string 442 | internal var _values = [String] () 443 | 444 | /// value set in an array of string, read only 445 | public var values: [String] { get { return _values } } 446 | 447 | /// constructor of Reference 448 | /// - parameters: 449 | /// - ldap: the LDAP handler 450 | /// - reference: the LDAPMessage (single element) 451 | public init(ldap:LDAP, reference:OpaquePointer) { 452 | var referrals = UnsafeMutablePointer?>(bitPattern: 0) 453 | 454 | // *NOTE* ldap_value_free is deprecated so have to use memfree in chain instead 455 | let r = ldap_parse_reference(ldap.ldap, reference, &referrals, nil, 0) 456 | guard r == 0 else { 457 | return 458 | }//end guard 459 | var cursor = referrals 460 | while(cursor != nil) { 461 | guard let pstr = cursor?.pointee else { 462 | break 463 | }//end guard 464 | _values.append(ldap.string(pstr: pstr)) 465 | ldap_memfree(pstr) 466 | cursor = cursor?.successor() 467 | }//end while 468 | ldap_memfree(referrals) 469 | }//end init 470 | }//end struct 471 | 472 | /// LDAP Result record 473 | internal struct Result { 474 | 475 | /// error code of result 476 | internal var _errCode = Int32(0) 477 | 478 | /// error code of result, read only 479 | public var errCode: Int { get { return Int(_errCode) } } 480 | 481 | /// error message 482 | internal var _errMsg = "" 483 | 484 | /// error message, read only 485 | public var errMsg: String { get { return _errMsg } } 486 | 487 | /// matched dn 488 | internal var _matched = "" 489 | 490 | /// matched dn, read only 491 | public var matched: String { get { return _matched } } 492 | 493 | /// referrals as an array of string 494 | internal var _ref = [String]() 495 | 496 | /// referrals as an array of string, read only 497 | public var referrals: [String] { get { return _ref } } 498 | 499 | /// constructor of Result 500 | /// - parameters: 501 | /// - ldap: the LDAP handler 502 | /// - result: the LDAPMessage (single element) 503 | public init(ldap:LDAP, result:OpaquePointer) { 504 | var emsg = UnsafeMutablePointer(bitPattern: 0) 505 | var msg = UnsafeMutablePointer(bitPattern: 0) 506 | var ref = UnsafeMutablePointer?>(bitPattern: 0) 507 | let r = ldap_parse_result(ldap.ldap, result, &_errCode, &msg, &emsg, &ref, nil, 0) 508 | guard r == 0 else { 509 | return 510 | }//end guard 511 | if msg != nil { 512 | _matched = ldap.string(pstr: msg!) 513 | ldap_memfree(msg) 514 | }//end if 515 | if emsg != nil { 516 | _errMsg = ldap.string(pstr: emsg!) 517 | ldap_memfree(emsg) 518 | }//end if 519 | var rf = ref 520 | while(rf != nil) { 521 | guard let p = rf?.pointee else { 522 | break 523 | } 524 | _ref.append(ldap.string(pstr: p)) 525 | ldap_memfree(p) 526 | rf = rf?.successor() 527 | }//end rf 528 | if ref != nil { 529 | ldap_memfree(ref) 530 | }//end if 531 | }//end Result 532 | } 533 | /// Result set of a searching query 534 | internal struct ResultSet { 535 | 536 | /// attribute set as an array 537 | internal var _attr = [AttributeSet]() 538 | 539 | /// attribute set as an array, read only 540 | public var attributeSet: [AttributeSet] { get { return _attr } } 541 | 542 | /// as an dictionary, read only 543 | public var dictionary:[String:[String:Any]] { get { 544 | var dic:[String:[String:Any]] = [:] 545 | for aset in _attr { 546 | var d: [String: Any] = [:] 547 | for a in aset.attributes { 548 | if a.values.count > 1 { 549 | d[a.name] = a.values 550 | }else { 551 | d[a.name] = a.values[0] 552 | }//end if 553 | }//next a 554 | dic[aset.name] = d 555 | }//next aset 556 | return dic 557 | } } //end simple 558 | 559 | /// references as an array 560 | internal var _ref = [Reference]() 561 | 562 | /// references as an array, read only 563 | public var references: [Reference] { get { return _ref } } 564 | 565 | /// results as an array of result 566 | internal var _results = [Result]() 567 | 568 | /// results as an array of result, read only 569 | public var result: [Result] { get { return _results } } 570 | 571 | /// constructor of Attribute 572 | /// - parameters: 573 | /// - ldap: the LDAP handler 574 | /// - chain: the LDAPMessage chain elements 575 | public init (ldap: LDAP, chain:OpaquePointer) { 576 | var m = ldap_first_message(ldap.ldap, chain) 577 | while(m != nil) { 578 | switch(UInt(ldap_msgtype(m))) { 579 | case LDAP_RES_SEARCH_ENTRY: 580 | _attr.append(AttributeSet(ldap: ldap, entry: m!)) 581 | case LDAP_RES_SEARCH_REFERENCE: 582 | _ref.append(Reference(ldap: ldap, reference: m!)) 583 | case LDAP_RES_SEARCH_RESULT: 584 | _results.append(Result(ldap: ldap, result: m!)) 585 | default: 586 | () 587 | }//end case 588 | m = ldap_next_message(ldap.ldap, m) 589 | }//end while 590 | }//end init 591 | }//end struct 592 | 593 | /// constant to indicate a sorting order: ascendant or descendant. 594 | public enum SortingOrder { 595 | case ASC 596 | case DSC 597 | }//end SortingOrder 598 | 599 | /// generate a standard sorting string from a series of fields 600 | /// - parameters: 601 | /// - sortedBy: an array of tuple, which tells each field to sort in what order 602 | /// - returns: 603 | /// the sorting language, as a string 604 | public static func sortingString( sortedBy: [(String, SortingOrder)] = [] ) -> String { 605 | return sortedBy.reduce("") { previous, next in 606 | let str = next.1 == .ASC ? next.0 : "-" + next.0 607 | return previous.isEmpty ? str : previous + " " + str 608 | }//end reduce 609 | }//end sortingString 610 | 611 | /// synchronized search 612 | /// - parameters: 613 | /// - base: String, search base domain (dn), default = "" 614 | /// - filter: String, the filter of query, default = "(objectclass=*)", means all possible results 615 | /// - scope: See Scope, BASE, SINGLE_LEVEL, SUBTREE or CHILDREN 616 | /// - sortedBy: a sorting string, may be generated by LDAP.sortingString() 617 | /// - returns: 618 | /// ResultSet. See ResultSet 619 | /// - throws: 620 | /// Exception.message 621 | public func search(base:String = "", filter:String = "(objectclass=*)", scope:Scope = .BASE, attributes: [String] = [], sortedBy: String = "") throws -> [String:[ 622 | String:Any]] { 623 | 624 | var serverControl = UnsafeMutablePointer(bitPattern: 0) 625 | 626 | if !sortedBy.isEmpty { 627 | var sortKeyList = UnsafeMutablePointer?>(bitPattern: 0) 628 | let sortString = ber_strdup(sortedBy) 629 | var r = ldap_create_sort_keylist(&sortKeyList, sortString) 630 | defer { ber_memfree(sortString) } 631 | guard r == 0 else { 632 | throw Exception.message(LDAP.error(r)) 633 | }//end if 634 | 635 | r = ldap_create_sort_control(self.ldap, sortKeyList, 0, &serverControl) 636 | defer { ldap_free_sort_keylist(sortKeyList) } 637 | guard r == 0 else { 638 | throw Exception.message(LDAP.error(r)) 639 | }//end if 640 | }//end if 641 | 642 | // prepare the return set 643 | var msg = OpaquePointer(bitPattern: 0) 644 | 645 | let r = withCArrayOfString(array: attributes) { pAttribute -> Int32 in 646 | 647 | // perform the search 648 | let result = ldap_search_ext_s(self.ldap, base, scope.rawValue, filter, pAttribute, 0, &serverControl, nil, nil, 0, &msg) 649 | 650 | if serverControl != nil { 651 | ldap_control_free(serverControl) 652 | } 653 | return result 654 | }//end 655 | 656 | if let m = msg { 657 | // process the result set 658 | let rs = ResultSet(ldap: self, chain: m) 659 | 660 | // release the memory 661 | ldap_msgfree(m) 662 | 663 | return rs.dictionary 664 | } 665 | 666 | throw Exception.message(LDAP.error(r)) 667 | }//end search 668 | 669 | /// asynchronized search 670 | /// - parameters: 671 | /// - base: String, search base domain (dn), default = "" 672 | /// - filter: String, the filter of query, default = "(objectclass=*)", means all possible results 673 | /// - scope: See Scope, BASE, SINGLE_LEVEL, SUBTREE or CHILDREN 674 | /// - sortedBy: a sorting string, may be generated by LDAP.sortingString() 675 | /// - completion: callback with a parameter of ResultSet, nil if failed 676 | 677 | public func search(base:String = "", filter:String = "(objectclass=*)", scope:Scope = .BASE, sortedBy: String = "", completion: @escaping ([String:[String:Any]])-> Void) { 678 | threading.async { 679 | var rs: [String:[String:Any]] = [:] 680 | do { 681 | rs = try self.search(base: base, filter: filter, scope: scope, sortedBy: sortedBy) 682 | }catch { 683 | rs = [:] 684 | }//end catch 685 | completion(rs) 686 | }//end threading 687 | }//end search 688 | 689 | /// allocate a modification structure for internal usage 690 | /// - parameters: 691 | /// - method: method of modification, i.e., LDAP_MOD_ADD or LDAP_MOD_REPLACE or LDAP_MOD_DELETE and LDAP_MOD_BVALUES 692 | /// - key: attribute name to modify 693 | /// - values: attribute values as an array 694 | /// - returns: 695 | /// an LDAPMod structure 696 | internal func modAlloc(method: Int32, key: String, values: [String]) -> LDAPMod { 697 | let pValues = values.map { self.string(str: $0) } 698 | let pointers = pValues.asUnsafeNullTerminatedPointers() 699 | return LDAPMod(mod_op: method, mod_type: ber_strdup(key), mod_vals: mod_vals_u(modv_bvals: pointers)) 700 | }//end modAlloc 701 | 702 | /// add an attribute to a DN 703 | /// - parameters: 704 | /// - distinguishedName: specific DN 705 | /// - attributes: attributes as an dictionary to add 706 | /// - throws: 707 | /// - Exception with message, such as no permission, or object class violation, etc. 708 | 709 | public func add(distinguishedName: String, attributes: [String:[String]]) throws { 710 | 711 | // map the keys to an array 712 | let keys:[String] = attributes.keys.map { $0 } 713 | 714 | // map the key array to a modification array 715 | let mods:[LDAPMod] = keys.map { self.modAlloc(method: LDAP_MOD_ADD | LDAP_MOD_BVALUES, key: $0, values: attributes[$0]!)} 716 | 717 | // get the pointers 718 | let pMods = mods.asUnsafeNullTerminatedPointers() 719 | 720 | // perform adding 721 | let r = ldap_add_ext_s(self.ldap, distinguishedName, pMods, nil, nil) 722 | 723 | // release memory 724 | ldap_mods_free(pMods, 0) 725 | 726 | guard r == 0 else { 727 | throw Exception.message(LDAP.error(r)) 728 | }//end if 729 | }//end func 730 | 731 | /// add an attribute to a DN 732 | /// - parameters: 733 | /// - distinguishedName: specific DN 734 | /// - attributes: attributes as an dictionary to add 735 | /// - completion: callback once done. If something wrong, an error message will pass to the closure. 736 | 737 | public func add(distinguishedName: String, attributes: [String:[String]],completion: @escaping (String?)-> Void) { 738 | 739 | threading.async { 740 | do { 741 | // perform adding 742 | try self.add(distinguishedName: distinguishedName, attributes: attributes) 743 | 744 | // if nothing wrong, callback 745 | completion(nil) 746 | 747 | }catch(let err) { 748 | 749 | // otherwise callback an error 750 | completion("\(err)") 751 | }//end do 752 | 753 | }//end dispatch 754 | }//end func 755 | 756 | /// modify an attribute to a DN 757 | /// - parameters: 758 | /// - distinguishedName: specific DN 759 | /// - attributes: attributes as an dictionary to modify 760 | /// - method: specify if an attribute should be added, removed or replaced (default) 761 | /// add: LDAP_MOD_ADD | LDAP_MOD_BVALUES 762 | /// remove: LDAP_MOD_DELETE | LDAP_MOD_BVALUES 763 | /// replace: LDAP_MOD_REPLACE | LDAP_MOD_BVALUES 764 | /// - throws: 765 | /// - Exception with message, such as no permission, or object class violation, etc. 766 | 767 | public func modify(distinguishedName: String, attributes: [String:[String]], method: Int32 = LDAP_MOD_REPLACE | LDAP_MOD_BVALUES) throws { 768 | 769 | // map the keys to an array 770 | let keys:[String] = attributes.keys.map { $0 } 771 | 772 | // map the key array to a modification array 773 | let mods:[LDAPMod] = keys.map { self.modAlloc(method: method, key: $0, values: attributes[$0]!)} 774 | 775 | // get the pointers 776 | let pMods = mods.asUnsafeNullTerminatedPointers() 777 | 778 | // perform modification 779 | let r = ldap_modify_ext_s(self.ldap, distinguishedName, pMods, nil, nil) 780 | 781 | // release memory 782 | ldap_mods_free(pMods, 0) 783 | 784 | guard r == 0 else { 785 | throw Exception.message(LDAP.error(r)) 786 | }//end if 787 | }//end func 788 | 789 | /// modify an attribute to a DN 790 | /// - parameters: 791 | /// - distinguishedName: specific DN 792 | /// - attributes: attributes as an dictionary to modify 793 | /// - completion: callback once done. If something wrong, an error message will pass to the closure. 794 | /// - method: specify if an attribute should be added, removed or replaced (default) 795 | /// add: LDAP_MOD_ADD | LDAP_MOD_BVALUES 796 | /// remove: LDAP_MOD_DELETE | LDAP_MOD_BVALUES 797 | /// replace: LDAP_MOD_REPLACE | LDAP_MOD_BVALUES 798 | 799 | public func modify(distinguishedName: String, attributes: [String:[String]],completion: @escaping (String?)-> Void, method: Int32 = LDAP_MOD_REPLACE | LDAP_MOD_BVALUES) { 800 | threading.async { 801 | do { 802 | // perform adding 803 | try self.modify(distinguishedName: distinguishedName, attributes: attributes, method: method) 804 | 805 | // if nothing wrong, callback 806 | completion(nil) 807 | 808 | }catch(let err) { 809 | 810 | // otherwise callback an error 811 | completion("\(err)") 812 | }//end do 813 | 814 | }//end dispatch 815 | }//end func 816 | 817 | /// delete an attribute to a DN 818 | /// - parameters: 819 | /// - distinguishedName: specific DN 820 | /// - attributes: attributes as an dictionary to delete 821 | /// - throws: 822 | /// - Exception with message, such as no permission, or object class violation, etc. 823 | 824 | public func delete(distinguishedName: String) throws { 825 | 826 | // perform deletion 827 | let r = ldap_delete_ext_s(self.ldap, distinguishedName, nil, nil) 828 | 829 | guard r == 0 else { 830 | throw Exception.message(LDAP.error(r)) 831 | }//end if 832 | } 833 | 834 | /// delete an attribute to a DN 835 | /// - parameters: 836 | /// - distinguishedName: specific DN 837 | /// - attributes: attributes as an dictionary to delete 838 | /// - completion: callback once done. If something wrong, an error message will pass to the closure. 839 | 840 | public func delete(distinguishedName: String, completion: @escaping (String?)-> Void) { 841 | threading.async { 842 | do { 843 | // perform adding 844 | try self.delete(distinguishedName: distinguishedName) 845 | 846 | // if nothing wrong, callback 847 | completion(nil) 848 | 849 | }catch(let err) { 850 | 851 | // otherwise callback an error 852 | completion("\(err)") 853 | }//end do 854 | 855 | }//end dispatch 856 | } 857 | }//end class 858 | --------------------------------------------------------------------------------