├── test-resources ├── cert.binary ├── server.keystore └── in-memeory-ds-schema ├── .gitignore ├── project.clj ├── test └── clj_ldap │ └── test │ ├── start_tls.clj │ ├── server.clj │ └── client.clj ├── CHANGELOG.md ├── README.md └── src └── clj_ldap └── client.clj /test-resources/cert.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldorman/clj-ldap/HEAD/test-resources/cert.binary -------------------------------------------------------------------------------- /test-resources/server.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldorman/clj-ldap/HEAD/test-resources/server.keystore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | classes 4 | lib 5 | target 6 | .lein* 7 | pom.xml 8 | pom.xml.asc 9 | .nrepl-port 10 | #*# 11 | .#* 12 | .DS_Store 13 | .idea 14 | clj-ldap.iml 15 | test-resources/access 16 | test-resources/access.* 17 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clojars.pntblnk/clj-ldap "0.0.17" 2 | :description "Clojure ldap client." 3 | :url "https://github.com/pauldorman/clj-ldap" 4 | :dependencies [[org.clojure/clojure "1.8.0"] 5 | [com.unboundid/unboundid-ldapsdk "5.1.1"]] 6 | :profiles {:test {:dependencies [[lein-clojars "0.9.1"]]}} 7 | :aot [clj-ldap.client] 8 | :license {:name "Eclipse Public License - v 1.0" 9 | :url "http://www.eclipse.org/legal/epl-v10.html" 10 | :distribution :repo 11 | :comments "same as Clojure"}) 12 | -------------------------------------------------------------------------------- /test/clj_ldap/test/start_tls.clj: -------------------------------------------------------------------------------- 1 | (ns clj-ldap.test.start-tls 2 | (:require [clojure.test :refer :all] 3 | [clj-ldap.client :as ldap] 4 | [clj-ldap.test.server :as server] 5 | [clj-ldap.test.client :as client]) 6 | (:import (com.unboundid.ldap.sdk LDAPConnectionPool))) 7 | 8 | (defn- test-server 9 | "Setup server" 10 | [f] 11 | (server/start!) 12 | (f) 13 | (server/stop!)) 14 | 15 | (defn- test-data 16 | "Provide test data" 17 | [f] 18 | (try 19 | (let [conn (ldap/connect {:host {:port (server/ldapPort)}})] 20 | (#'client/add-toplevel-objects! conn) 21 | (ldap/add conn (:dn client/person-a*) (:object client/person-a*))) 22 | (catch Exception e)) 23 | (f)) 24 | 25 | (use-fixtures :once test-server) 26 | (use-fixtures :each test-data) 27 | 28 | (deftest start-tls-test 29 | (testing "Connect with :bind-dn and StartTLS returns a valid connection pool" 30 | (is (= LDAPConnectionPool 31 | (type (ldap/connect {:host {:address "localhost" 32 | :port (server/ldapPort)} 33 | :startTLS? true 34 | :bind-dn (str "cn=testa," client/base*) 35 | :password "passa"})))))) -------------------------------------------------------------------------------- /test/clj_ldap/test/server.clj: -------------------------------------------------------------------------------- 1 | (ns clj-ldap.test.server 2 | "An embedded ldap server for unit testing" 3 | (:require [clj-ldap.client :as ldap]) 4 | (:import [com.unboundid.ldap.listener 5 | InMemoryDirectoryServerConfig 6 | InMemoryDirectoryServer 7 | InMemoryListenerConfig]) 8 | (:import [com.unboundid.ldap.sdk.schema 9 | Schema]) 10 | (:import [com.unboundid.util.ssl 11 | KeyStoreKeyManager 12 | SSLUtil 13 | TrustAllTrustManager]) 14 | (:import (java.io File) 15 | (java.util.logging FileHandler Level) 16 | (com.unboundid.util MinimalLogFormatter))) 17 | 18 | ;; server will hold an InMemoryDirectoryServer instance 19 | (defonce server (atom nil)) 20 | 21 | (defn- createAccessLogger 22 | "Let the server write protocol information to test-resources/access" 23 | [] 24 | (let [logFile (File. "test-resources/access") 25 | fileHandler (FileHandler. (.getAbsolutePath logFile) true)] 26 | (.setLevel fileHandler Level/INFO) 27 | (.setFormatter fileHandler (MinimalLogFormatter. nil false false true)) 28 | fileHandler)) 29 | 30 | ;; Server's certificate, with alias "server-cert" generated using keytool like so: 31 | ;; keytool -genkey -keyalg RSA -alias server-cert -keystore selfsigned.jks -validity 3650 -keysize 2048 32 | 33 | (defn- start-ldap-server 34 | "Setup a server listening on available LDAP and LDAPS ports chosen at random" 35 | [] 36 | (let [cfg (InMemoryDirectoryServerConfig. (into-array String ["dc=alienscience,dc=org,dc=uk"])) 37 | _ (.addAdditionalBindCredentials cfg "cn=Directory Manager" "password") 38 | _ (.setSchema cfg (Schema/getDefaultStandardSchema)) 39 | _ (.setAccessLogHandler cfg (createAccessLogger)) 40 | keystore (KeyStoreKeyManager. "test-resources/server.keystore" 41 | (char-array "password") "JKS" "server-cert") 42 | serverSSLUtil (SSLUtil. keystore (TrustAllTrustManager.)) 43 | clientSSLUtil (SSLUtil. (TrustAllTrustManager.)) 44 | _ (.setListenerConfigs cfg 45 | [(InMemoryListenerConfig/createLDAPConfig 46 | "LDAP" nil 0 47 | (.createSSLSocketFactory serverSSLUtil)) 48 | (InMemoryListenerConfig/createLDAPSConfig 49 | "LDAPS" nil 0 50 | (.createSSLServerSocketFactory serverSSLUtil) 51 | (.createSSLSocketFactory clientSSLUtil))]) 52 | ds (InMemoryDirectoryServer. cfg)] 53 | (.startListening ds) 54 | ds)) 55 | 56 | (defn stop! 57 | "Stops the embedded ldap server (listening on LDAP and LDAPS ports)" 58 | [] 59 | (if @server 60 | (do 61 | (.shutDown @server true) 62 | (reset! server nil)))) 63 | 64 | (defn ldapPort 65 | [] 66 | (.getListenPort @server "LDAP")) 67 | 68 | (defn ldapsPort 69 | [] 70 | (.getListenPort @server "LDAPS")) 71 | 72 | (defn start! 73 | "Starts an embedded ldap server on the given port and SSL" 74 | [] 75 | (stop!) 76 | (reset! server (start-ldap-server))) 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [0.0.17] 6 | This release adds the following: 7 | - A `bind` function which behaves similarly to `bind?` except that it throws `LDAPException` on failure. Thanks to @yogthos and @sumbach. 8 | - A bump of the ldap-sdk to 5.1.1. 9 | 10 | ## [0.0.16] 11 | This release adds the following: 12 | - A lazy-seq returned by `search-all-results` using SimplePagedResults control. Thanks to @jindrichmynarz. 13 | 14 | ## [0.0.15] 15 | This release adds the following: 16 | - A fix of StartTLS support. 17 | 18 | ## [0.0.14] 19 | This release adds the following: 20 | - The :initial-connections and :max-connections options to the connect function, without deprecating the :num-connections option. 21 | 22 | ## [0.0.13] 23 | This release adds the following: 24 | - The :delete-subtree option to the delete function, which requires the support of the Subtree Delete Request Control on the server 25 | 26 | ## [0.0.12] 27 | This release adds the following: 28 | - The :proxied-auth option to most functions 29 | - The compare? function which invokes LDAP compare operation 30 | - get-connection and release-connection functions which promote proper use of the connection pool when a sequence of operations must be performed on a single connection 31 | 32 | ## [0.0.11] 33 | This release addresses a bug introduced in 0.0.10 and bumps the ldap-sdk version to 3.1.1. 34 | - Fix bug preventing use of :pre-read and :post-read options to modify and delete 35 | - Introduce test coverage of :pre-read and :post-read 36 | 37 | ## [0.0.10] 38 | ### Binary (byte-array) attribute values 39 | The behavior of the api towards binary valued attributes has been fixed. Thanks to Ray Miller for providing the 40 | changes to `create-modification`. In addition, the api now returns binary data as it was orginally submitted as opposed 41 | to base64 encoded. Base64 encoding is only needed if the client requests a text representation of the LDAP entry, such as LDIF. A byte-valued collection of attribute names (as keywords) can be provided to all search functions to instruct the api to return 42 | byte arrays as opposed to strings. 43 | 44 | ### Bind? Fix 45 | A bug was discovered and fixed in `bind?` which would leave connections in the pool with an authorization ID 46 | associated with previous BIND. Thanks to Adam Harper and Sam Umbach for debugging. The `bind?` function 47 | now acts as follows: 48 | - If passed a connection pool (which is the default behavior, `connect` returns a connection pool) then the BIND is 49 | performed with a `pool.bindAndRevertAuthentication` which returns the autherization ID back to what it was before the BIND. 50 | - If passed an individual LDAPConnection (which can be retrieved via `(.getConnection pool)`, then 51 | the BIND is performed as usual and the caller is responsible for using `(.releaseAndReAuthenticateConnection pool conn)` 52 | to release the connection back to the pool. 53 | 54 | ### Added 55 | - size-limit, time-limit and types-only options to search. 56 | - StartTLS support with the startTLS? boolean option to connect. 57 | - byte-valued option to search functions which implies byte array values are to be returned for these attributes. 58 | - controls option to search. This allows passing in arbitrary controls which have been properly instantiated via java interOp. 59 | - respf option to search. This function, if defined, will be invoked on the list of response controls, if present. 60 | - server-sort option to search which attaches a ServerSideSortRequestControl to the search operation. 61 | - Who Am I? extended request. 62 | - Unit tests for the above. 63 | 64 | ### Replaced 65 | - test.server has been rewritten to incorporate the InMemoryDirectoryServer provided in the UnboundID LDAP SDK. 66 | -------------------------------------------------------------------------------- /test/clj_ldap/test/client.clj: -------------------------------------------------------------------------------- 1 | (ns clj-ldap.test.client 2 | "Automated tests for clj-ldap" 3 | (:require [clj-ldap.client :as ldap] 4 | [clj-ldap.test.server :as server]) 5 | (:use clojure.test) 6 | (:import (com.unboundid.ldap.sdk LDAPException))) 7 | 8 | 9 | ;; Tests are run over a variety of connection types (LDAP and LDAPS for now) 10 | (def ^:dynamic *connections* nil) 11 | (def ^:dynamic *conn* nil) 12 | (def ^:dynamic *c* nil) 13 | 14 | ;; Tests concentrate on a single object class 15 | (def toplevel* "dc=alienscience,dc=org,dc=uk") 16 | (def base* (str "ou=people," toplevel*)) 17 | (def dn* (str "cn=%s," base*)) 18 | (def object-class* #{"top" "person"}) 19 | 20 | ;; Variable to catch side effects 21 | (def ^:dynamic *side-effects* nil) 22 | 23 | ;; Result of a successful write 24 | (def success* {:code 0 :name "success"}) 25 | 26 | (defn read-bytes-from-file 27 | [filename] 28 | (let [f (java.io.File. filename) 29 | ary (byte-array (.length f)) 30 | is (java.io.FileInputStream. f)] 31 | (.read is ary) 32 | (.close is) 33 | ary)) 34 | 35 | ;; People to test with 36 | (def person-a* 37 | {:dn (format dn* "testa") 38 | :object {:objectClass object-class* 39 | :cn "testa" 40 | :sn "a" 41 | :description "description a" 42 | :telephoneNumber "000000001" 43 | :userPassword "passa"}}) 44 | 45 | (def person-b* 46 | {:dn (format dn* "testb") 47 | :object {:objectClass object-class* 48 | :cn "testb" 49 | :sn "b" 50 | :description "István Orosz" 51 | :telephoneNumber ["000000002" "00000003"] 52 | :userPassword "passb"}}) 53 | 54 | (def person-c* 55 | {:dn (format dn* "André Marchand") 56 | :object {:objectClass object-class* 57 | :cn "André Marchand" 58 | :sn "Marchand" 59 | :description "description c" 60 | :telephoneNumber "000000004" 61 | :userPassword "passc"}}) 62 | 63 | (defn- connect-to-server 64 | "Opens a sequence of connection pools on the localhost server with the 65 | given ports" 66 | [port ssl-port] 67 | [ 68 | (ldap/connect {:host {:port port}}) 69 | (ldap/connect {:host {:address "localhost" 70 | :port port} 71 | :num-connections 4}) 72 | (ldap/connect {:host (str "localhost:" port)}) 73 | (ldap/connect {:ssl? true 74 | :host {:port ssl-port} 75 | :initial-connections 2}) 76 | (ldap/connect {:starTLS? true 77 | :host {:port port}}) 78 | (ldap/connect {:host {:port port} 79 | :connect-timeout 1000 80 | :timeout 5000 81 | :max-connections 2}) 82 | (ldap/connect {:host [(str "localhost:" port) 83 | {:port ssl-port}]}) 84 | (ldap/connect {:host [(str "localhost:" ssl-port) 85 | {:port ssl-port}] 86 | :ssl? true 87 | :num-connections 5 88 | :max-connections 10})]) 89 | 90 | 91 | (defn- test-server 92 | "Setup server" 93 | [f] 94 | (server/start!) 95 | (binding [*connections* (connect-to-server (server/ldapPort) (server/ldapsPort))] 96 | (f)) 97 | (server/stop!)) 98 | 99 | (defn- add-toplevel-objects! 100 | "Adds top level entries, needed for testing, to the ldap server" 101 | [connection] 102 | (ldap/add connection toplevel* 103 | {:objectClass ["top" "domain" "extensibleObject"] 104 | :dc "alienscience"}) 105 | (ldap/add connection base* 106 | {:objectClass ["top" "organizationalUnit"] 107 | :ou "people"}) 108 | (ldap/add connection 109 | (str "cn=Saul Hazledine," base*) 110 | {:objectClass ["top" "Person"] 111 | :cn "Saul Hazledine" 112 | :sn "Hazledine" 113 | :description "Creator of bugs"})) 114 | 115 | (defn- test-data 116 | "Provide test data" 117 | [f] 118 | (doseq [connection *connections*] 119 | (binding [*conn* connection] 120 | (try 121 | (add-toplevel-objects! *conn*) 122 | (ldap/add *conn* (:dn person-a*) (:object person-a*)) 123 | (ldap/add *conn* (:dn person-b*) (:object person-b*)) 124 | (catch Exception e)) 125 | (f) 126 | (try 127 | (ldap/delete *conn* toplevel* {:delete-subtree true}) 128 | (catch Exception e))))) 129 | 130 | (use-fixtures :each test-data) 131 | (use-fixtures :once test-server) 132 | 133 | (deftest test-get 134 | (is (= (assoc (:object person-a*) :dn (:dn person-a*)) 135 | (ldap/get *conn* (:dn person-a*)))) 136 | (is (= (assoc (:object person-b*) :dn (:dn person-b*)) 137 | (ldap/get *conn* (:dn person-b*)))) 138 | (is (= {:dn (:dn person-a*) 139 | :cn (-> person-a* :object :cn) 140 | :sn (-> person-a* :object :sn)} 141 | (ldap/get *conn* (:dn person-a*) [:cn :sn])))) 142 | 143 | (deftest some-bind 144 | (is (= {:code 0, :name "success"} 145 | (ldap/bind *conn* (:dn person-a*) "passa"))) 146 | (is (thrown? LDAPException (ldap/bind *conn* (:dn person-a*) "notthepass"))) 147 | (is (thrown? LDAPException (ldap/bind *conn* "cn=does,ou=not,cn=exist" "password")))) 148 | 149 | (deftest bad-bind? 150 | (is (= false (ldap/bind? *conn* (:dn person-a*) "not-the-password"))) 151 | (is (= false (ldap/bind? *conn* "cn=does,ou=not,cn=exist" "password")))) 152 | 153 | (deftest test-bind 154 | (if (> (-> *conn* 155 | (.getConnectionPoolStatistics) 156 | (.getMaximumAvailableConnections)) 1) 157 | (binding [*c* (ldap/get-connection *conn*)] 158 | (let [before (ldap/who-am-i *c*) 159 | _ (ldap/bind? *c* (:dn person-a*) "passa") 160 | a (ldap/who-am-i *c*) 161 | _ (ldap/release-connection *conn* *c*)] 162 | (is (= ["" (:dn person-a*)] [before a])))))) 163 | 164 | (deftest test-add-delete 165 | (is (= success* (ldap/add *conn* (:dn person-c*) (:object person-c*)))) 166 | (is (= (assoc (:object person-c*) :dn (:dn person-c*)) 167 | (ldap/get *conn* (:dn person-c*)))) 168 | (is (= success* (ldap/delete *conn* (:dn person-c*)))) 169 | (is (nil? (ldap/get *conn* (:dn person-c*)))) 170 | (is (= success* 171 | (ldap/add *conn* (str "changeNumber=1234," base*) 172 | {:objectClass ["changeLogEntry"] 173 | :changeNumber 1234 174 | :targetDN base* 175 | :changeType "modify"}))) 176 | (is (= "1234" (:changeNumber (ldap/get *conn* (str "changeNumber=1234," base*))))) 177 | (is (= {:code 0, :name "success", 178 | :pre-read {:objectClass #{"top" "changeLogEntry"}}} 179 | (ldap/delete *conn* (str "changeNumber=1234," base*) 180 | {:pre-read [:objectClass]})))) 181 | 182 | (deftest test-delete-subtree 183 | (is (= success* (ldap/add *conn* (:dn person-c*) (:object person-c*)))) 184 | (is (= success* (ldap/delete *conn* base* {:delete-subtree true}))) 185 | (is (nil? (ldap/get *conn* base*)))) 186 | 187 | (deftest test-modify-add 188 | (is (= {:code 0, :name "success", 189 | :pre-read {:objectClass #{"top" "person"}, :cn "testa"}, 190 | :post-read {:l "Hollywood", :cn "testa"}} 191 | (ldap/modify *conn* (:dn person-a*) 192 | {:add {:objectClass "organizationalPerson" 193 | :l "Hollywood"} 194 | :pre-read #{:objectClass :l :cn} 195 | :post-read #{:l :cn}}))) 196 | (is (= success* 197 | (ldap/modify *conn* (:dn person-b*) 198 | {:add {:telephoneNumber ["0000000005" "0000000006"]}}))) 199 | (let [new-a (ldap/get *conn* (:dn person-a*)) 200 | new-b (ldap/get *conn* (:dn person-b*)) 201 | obj-a (:object person-a*) 202 | obj-b (:object person-b*)] 203 | (is (= (conj (:objectClass obj-a) "organizationalPerson") 204 | (:objectClass new-a))) 205 | (is (= "Hollywood" (:l new-a))) 206 | (is (= (set (concat (:telephoneNumber obj-b) 207 | ["0000000005" "0000000006"])) 208 | (set (:telephoneNumber new-b)))))) 209 | 210 | (deftest test-modify-delete 211 | (let [b-phonenums (-> person-b* :object :telephoneNumber)] 212 | (is (= success* 213 | (ldap/modify *conn* (:dn person-a*) 214 | {:delete {:description :all}}))) 215 | (is (= success* 216 | (ldap/modify *conn* (:dn person-b*) 217 | {:delete {:telephoneNumber (first b-phonenums)}}))) 218 | (is (= (-> (:object person-a*) 219 | (dissoc :description) 220 | (assoc :dn (:dn person-a*))) 221 | (ldap/get *conn* (:dn person-a*)))) 222 | (is (= (-> (:object person-b*) 223 | (assoc :telephoneNumber (second b-phonenums)) 224 | (assoc :dn (:dn person-b*))) 225 | (ldap/get *conn* (:dn person-b*)))))) 226 | 227 | (deftest test-modify-replace 228 | (let [new-phonenums (-> person-b* :object :telephoneNumber) 229 | certificate-data (read-bytes-from-file 230 | "test-resources/cert.binary")] 231 | (is (= success* 232 | (ldap/modify *conn* (:dn person-a*) 233 | {:replace {:telephoneNumber new-phonenums}}))) 234 | (is (= (-> (:object person-a*) 235 | (assoc :telephoneNumber new-phonenums) 236 | (assoc :dn (:dn person-a*))) 237 | (ldap/get *conn* (:dn person-a*)))) 238 | 239 | (is (= success* 240 | (ldap/modify *conn* (:dn person-a*) 241 | {:add {:objectclass ["inetOrgPerson" 242 | "organizationalPerson"] 243 | :userCertificate certificate-data}} 244 | {:proxied-auth (str "dn:" (:dn person-a*))}))) 245 | (is (= (seq certificate-data) 246 | (seq (:userCertificate 247 | (first (ldap/search *conn* (:dn person-a*) 248 | {:scope :base 249 | :filter "(objectclass=inetorgperson)" 250 | :attributes [:userCertificate] 251 | :byte-valued [:userCertificate]})))))) 252 | (is (= (seq certificate-data) 253 | (seq (:userCertificate 254 | (first (ldap/search *conn* (:dn person-a*) 255 | {:scope :base 256 | :byte-valued [:userCertificate]})))))) 257 | (is (= (seq certificate-data) 258 | (seq (:userCertificate (ldap/get *conn* (:dn person-a*) 259 | [:userCertificate] 260 | [:userCertificate]))))))) 261 | 262 | (deftest test-modify-all 263 | (let [b (:object person-b*) 264 | b-phonenums (:telephoneNumber b)] 265 | (is (= success* 266 | (ldap/modify *conn* (:dn person-b*) 267 | {:add {:telephoneNumber "0000000005"} 268 | :delete {:telephoneNumber (second b-phonenums)} 269 | :replace {:description "desc x"}}))) 270 | (let [new-b (ldap/get *conn* (:dn person-b*))] 271 | (is (= (set [(first b-phonenums) "0000000005"]) 272 | (set (:telephoneNumber new-b)))) 273 | (is (= "desc x" (:description new-b)))))) 274 | 275 | (deftest test-search 276 | (is (= (set [nil "testa" "testb" "Saul Hazledine"]) 277 | (set (map :cn 278 | (ldap/search *conn* base* {:attributes [:cn]}))))) 279 | (is (= (set ["testa" "testb"]) 280 | (set (map :cn 281 | (ldap/search *conn* base* 282 | {:attributes [:cn] 283 | :filter "cn=test*" 284 | :proxied-auth (str "dn:" (:dn person-a*))}))))) 285 | (is (= '("Saul Hazledine" "testa" "testb") 286 | (map :cn 287 | (ldap/search *conn* base* 288 | {:filter "cn=*" 289 | :server-sort {:is-critical true 290 | :sort-keys [:cn :ascending]}})))) 291 | 292 | (is (= 2 (count (map :cn 293 | (ldap/search *conn* base* 294 | {:attributes [:cn] :filter "cn=*" 295 | :size-limit 2}))))) 296 | (is (= nil (:description (map :cn 297 | (ldap/search *conn* base* 298 | {:attributes [:cn] 299 | :filter "cn=István Orosz" 300 | :types-only true}))))) 301 | (is (= (set ["István Orosz"]) 302 | (set (map :description 303 | (ldap/search *conn* base* 304 | {:filter "cn=testb" :types-only false}))))) 305 | (binding [*side-effects* #{}] 306 | (ldap/search! *conn* base* {:attributes [:cn :sn] :filter "cn=test*"} 307 | (fn [x] 308 | (set! *side-effects* 309 | (conj *side-effects* (dissoc x :dn))))) 310 | (is (= (set [{:cn "testa" :sn "a"} 311 | {:cn "testb" :sn "b"}]) 312 | *side-effects*)))) 313 | 314 | 315 | (deftest test-search-all 316 | (let [options {:attributes [:cn] :page-size 2} 317 | lazy-results (ldap/search-all *conn* base* options) 318 | eager-results (ldap/search *conn* base* options) 319 | ->cn-set (comp set (partial map :cn))] 320 | (testing "Since search-all is lazy, not all results are fetched" 321 | (is (not (realized? lazy-results)))) 322 | (testing "Lazy and eager search eventually produce the same results" 323 | (is (->cn-set lazy-results) 324 | (->cn-set eager-results))))) 325 | 326 | (deftest test-compare? 327 | (is (= true (ldap/compare? *conn* (:dn person-b*) 328 | :description "István Orosz"))) 329 | (is (= false (ldap/compare? *conn* (:dn person-a*) 330 | :description "István Orosz" 331 | {:proxied-auth (str "dn:" (:dn person-b*))})))) 332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Introduction 3 | 4 | clj-ldap is a thin layer on the [unboundid sdk](http://www.unboundid.com/products/ldap-sdk/) and allows clojure programs to talk to ldap servers. This library is available on [clojars.org](http://clojars.org/search?q=clj-ldap) 5 | ```clojure 6 | :dependencies [[org.clojars.pntblnk/clj-ldap "0.0.17"]] 7 | ``` 8 | # Example 9 | 10 | ```clojure 11 | (ns example 12 | (:require [clj-ldap.client :as ldap])) 13 | 14 | (def ldap-server (ldap/connect {:host "ldap.example.com"})) 15 | 16 | (ldap/get ldap-server "cn=dude,ou=people,dc=example,dc=com") 17 | 18 | ;; Returns a map such as 19 | {:gidNumber "2000" 20 | :loginShell "/bin/bash" 21 | :objectClass #{"inetOrgPerson" "posixAccount" "shadowAccount"} 22 | :mail "dude@example.com" 23 | :sn "Dudeness" 24 | :cn "dude" 25 | :uid "dude" 26 | :homeDirectory "/home/dude"} 27 | ``` 28 | 29 | # API 30 | 31 | ## connect [options] 32 | 33 | Connects to an ldap server and returns a thread safe [LDAPConnectionPool](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPConnectionPool.html). 34 | Options is a map with the following entries: 35 | 36 | :host Either a string in the form "address:port" 37 | OR a map containing the keys, 38 | :address defaults to localhost 39 | :port defaults to 389 (or 636 for ldaps), 40 | OR a collection containing multiple hosts used for load 41 | balancing and failover. This entry is optional. 42 | :bind-dn The DN to bind as, optional 43 | :password The password to bind with, optional 44 | :num-connections Establish a fixed size connection pool. Defaults to 1. 45 | :initial-connections Establish a connection pool initially of this size with 46 | capability to grow to :max-connections. Defaults to 1. 47 | :max-connections Define maximum size of connection pool. It must be 48 | greater than or equal to the initial number of 49 | connections, defaults to value of :initial-connections. 50 | :ssl? Boolean, connect over SSL (ldaps), defaults to false 51 | :startTLS? Boolean, use startTLS over non-SSL port, defaults to false 52 | :trust-store Only trust SSL certificates that are in this 53 | JKS format file, optional, defaults to trusting all 54 | certificates 55 | :connect-timeout The timeout for making connections (milliseconds), 56 | defaults to 1 minute 57 | :timeout The timeout when waiting for a response from the server 58 | (milliseconds), defaults to 5 minutes 59 | 60 | Throws an [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) if an error occurs establishing the connection pool or authenticating to any of the servers. 61 | Some examples: 62 | ```clojure 63 | (ldap/connect {:host "ldap.example.com" 64 | :num-connections 4 65 | :bind-dn "cn=admin,dc=example,dc=com" 66 | :password "password"}) 67 | 68 | (ldap/connect {:host [{:address "ldap1.example.com" :port 1389} 69 | {:address "ldap3.example.com"} 70 | "ldap2.example.com:1389"] 71 | :startTLS? true 72 | :initial-connections 9 73 | :max-connections 18 74 | :bind-dn "cn=directory manager" 75 | :password "password"}) 76 | 77 | (ldap/connect {:host {:port 1389} 78 | :bind-dn "cn=admin,dc=example,dc=com" 79 | :password "password"}) 80 | ``` 81 | The pool can then be used as a parameter to all the functions in the library where 82 | a connection is expected. Using a pool in this manner aleviates the caller from having to get and 83 | release connections. It will still be necessary to get and release a connection if a single 84 | connection is needed to process a sequence of operations. See the following bind? example. 85 | 86 | ## bind [connection bind-dn password] [connection-pool bind-dn password] 87 | 88 | Usage: 89 | ```clojure 90 | ;; Authenticate user with given dn but the pool retains the previous identity on the connection 91 | (try 92 | (ldap/bind pool "cn=dude,ou=people,dc=example,dc=com" "somepass") 93 | (catch LDAPException e 94 | (if (= 49 (.intValue (.getResultCode e))) 95 | (prn "Password invalid") 96 | (throw e)))) 97 | 98 | (let [conn (ldap/get-connection pool) 99 | user-dn "uid=user.1,ou=people,dc=example,dc=com" 100 | user-password "password"] 101 | (try 102 | ;; Passed a connection, a successful authentication changes the authorization identity of the connection 103 | (when (ldap/bind? conn user-dn user-password) 104 | (ldap/modify conn user-dn {:replace {:description "On sabatical"}})) 105 | (finally (ldap/release-connection pool conn)))) 106 | ``` 107 | Performs a bind operation using the provided connection, bindDN and 108 | password. Returns true if successful and false otherwise. 109 | 110 | When an LDAP connection object is used as the connection argument the 111 | bind? function will attempt to change the identity of that connection 112 | to that of the provided DN. Subsequent operations on that connection 113 | will be done using the bound identity. 114 | 115 | If an LDAP connection pool object is passed as the connection argument 116 | the bind attempt will have no side-effects, leaving the state of the 117 | underlying connections unchanged. 118 | 119 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) on unsuccessful authentication or other error. 120 | 121 | ## bind? [connection bind-dn password] [connection-pool bind-dn password] 122 | 123 | Usage: 124 | ```clojure 125 | ;; Authenticate user with given dn but the pool retains the previous identity on the connection 126 | (ldap/bind? pool "cn=dude,ou=people,dc=example,dc=com" "somepass") 127 | 128 | (let [conn (ldap/get-connection pool) 129 | user-dn "uid=user.1,ou=people,dc=example,dc=com" 130 | user-password "password"] 131 | (try 132 | ;; Passed a connection, a successful authentication changes the authorization identity of the connection 133 | (when (ldap/bind? conn user-dn user-password) 134 | (ldap/modify conn user-dn {:replace {:description "On sabatical"}})) 135 | (finally (ldap/release-connection pool conn)))) 136 | ``` 137 | Performs a bind operation using the provided connection, bindDN and 138 | password. Returns true if successful and false otherwise. 139 | 140 | When an LDAP connection object is used as the connection argument the 141 | bind? function will attempt to change the identity of that connection 142 | to that of the provided DN. Subsequent operations on that connection 143 | will be done using the bound identity. 144 | 145 | If an LDAP connection pool object is passed as the connection argument 146 | the bind attempt will have no side-effects, leaving the state of the 147 | underlying connections unchanged. 148 | 149 | ## get [connection dn] [connection dn attributes] 150 | 151 | If successful, returns a map containing the entry for the given DN. 152 | Returns nil if the entry doesn't exist. 153 | ```clojure 154 | (ldap/get conn "cn=dude,ou=people,dc=example,dc=com") 155 | ``` 156 | Takes an optional collection that specifies which attributes will be returned from the server. 157 | ```clojure 158 | (ldap/get conn "cn=dude,ou=people,dc=example,dc=com" [:cn :sn]) 159 | ``` 160 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) on error. 161 | 162 | ## add [connection dn entry] 163 | 164 | Adds an entry to the connected ldap server. The entry is map of keywords to values which can be strings, sets or vectors. 165 | 166 | ```clojure 167 | (ldap/add conn "cn=dude,ou=people,dc=example,dc=com" 168 | {:objectClass #{"top" "person"} 169 | :cn "dude" 170 | :sn "a" 171 | :description "His dudeness" 172 | :telephoneNumber ["1919191910" "4323324566"]}) 173 | ``` 174 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) if there is an error with the request or the add failed. 175 | 176 | ## compare? [connection dn attribute assertion-value] 177 | 178 | Determines if the specified entry contains the given attribute and value. 179 | 180 | ```clojure 181 | (ldap/compare? conn "cn=dude,ou=people,dc=example,dc=com" 182 | :description "His dudeness") 183 | ``` 184 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) if there is an error with the request or the LDAP compare failed. 185 | 186 | ## modify [connection dn modifications] 187 | 188 | Modifies an entry in the connected ldap server. The modifications are 189 | a map in the form: 190 | ```clojure 191 | {:add 192 | {:attribute-a some-value 193 | :attribute-b [value1 value2]} 194 | :delete 195 | {:attribute-c :all 196 | :attribute-d some-value 197 | :attribute-e [value1 value2]} 198 | :replace 199 | {:attibute-d value 200 | :attribute-e [value1 value2]} 201 | :increment 202 | {:attribute-f value} 203 | :pre-read 204 | #{:attribute-a :attribute-b} 205 | :post-read 206 | #{:attribute-c :attribute-d}} 207 | ``` 208 | Where :add adds an attribute value, :delete deletes an attribute value and :replace replaces the set of values for the attribute with the ones specified. The entries :pre-read and :post-read specify attributes that have be read and returned either before or after the modifications have taken place. 209 | 210 | All the keys in the map are optional e.g: 211 | ```clojure 212 | (ldap/modify conn "cn=dude,ou=people,dc=example,dc=com" 213 | {:add {:telephoneNumber "232546265"}}) 214 | ``` 215 | The values in the map can also be set to :all when doing a delete e.g: 216 | ```clojure 217 | (ldap/modify conn "cn=dude,ou=people,dc=example,dc=com" 218 | {:delete {:telephoneNumber :all}} 219 | {:proxied-auth "dn:cn=app,dc=example,dc=com"}) 220 | ``` 221 | The values of the attributes given in :pre-read and :post-read are available in the returned map and are part of an atomic ldap operation e.g 222 | ```clojure 223 | (ldap/modify conn "uid=maxuid,ou=people,dc=example,dc=com" 224 | {:increment {:uidNumber 1} 225 | :post-read #{:uidNumber}}) 226 | ``` 227 | returns 228 | ```clojure 229 | {:code 0 230 | :name "success" 231 | :post-read {:uidNumber "2002"}} 232 | ``` 233 | The above technique can be used to maintain counters for unique ids as described by [rfc4525](http://tools.ietf.org/html/rfc4525). 234 | 235 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) on error. 236 | 237 | ## search [connection base] [connection base options] 238 | 239 | Runs a search on the connected ldap server, reads all the results into 240 | memory and returns the results as a sequence of maps. An introduction 241 | to ldap searching can be found in this [article](http://www.enterprisenetworkingplanet.com/netsysm/article.php/3317551/Unmasking-the-LDAP-Search-Filter.htm). 242 | 243 | Options is a map with the following optional entries: 244 | 245 | :scope The search scope, can be :base :one :sub or :subordinate, 246 | defaults to :sub 247 | :filter A string representing the search filter, 248 | defaults to "(objectclass=*)" 249 | :attributes A collection of the attributes to return, 250 | defaults to all user attributes 251 | :byte-valued A collection of attributes to return as byte arrays as 252 | opposed to Strings. 253 | :size-limit The maximum number of entries that the server should return 254 | :time-limit The maximum length of time in seconds that the server should 255 | spend processing this request 256 | :types-only Return only attribute names instead of names and values 257 | :server-sort Instruct the server to sort the results. The value of this 258 | key is a map like the following: 259 | { :is-critical ( true | false ) 260 | :sort-keys [ :cn :ascending 261 | :employeNumber :descending ... ] } 262 | At least one sort key must be provided. 263 | :proxied-auth The dn: or u: to be used as the authorization 264 | identity when processing the request. Don't forget the dn:/u: prefix. 265 | :controls Adds the provided controls for this request. 266 | :respf Applies this function to the list of response controls present. 267 | 268 | e.g 269 | ```clojure 270 | (ldap/search conn "ou=people,dc=example,dc=com") 271 | 272 | (ldap/search conn "ou=people,dc=example,dc=com" {:attributes [:cn] :sizelimit 100 273 | :proxied-auth "dn:cn=app,dc=example,dc=com"}) 274 | 275 | (ldap/search conn "dc=example,dc=com" {:filter "(uid=abc123)" :attributes [:cn :uid :userCertificate] 276 | :byte-valued [:userCertificate]}) 277 | ``` 278 | Throws a [LDAPSearchException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPSearchException.html) on error. This function will not throw the exception in the event 279 | of a size limit exceeded result, instead the entries are returned. 280 | 281 | ## search! [connection base f] [connection base options f] 282 | 283 | Runs a search on the connected ldap server and executes the given 284 | function (for side effects) on each result. Does not read all the 285 | results into memory. The options argument is a map similar to that of the search 286 | function defined above. e.g 287 | ```clojure 288 | (ldap/search! conn "ou=people,dc=example,dc=com" println) 289 | 290 | (ldap/search! conn "ou=people,dc=example,dc=com" 291 | {:filter "sn=dud*"} 292 | (fn [x] 293 | (println "Hello " (:cn x)))) 294 | ``` 295 | Throws a [LDAPSearchException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPSearchException.html) if an error occurs during search. Throws an [EntrySourceException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/EntrySourceException.html) if there is an error obtaining search results. 296 | 297 | ## delete [connection dn] [connection dn options] 298 | 299 | Deletes the given entry in the connected ldap server. 300 | 301 | Options is a map with the following optional entries: 302 | 303 | :delete-subtree Use the Subtree Delete Control to delete entire subtree rooted at dn. 304 | :pre-read A set of attributes that should be read before deletion (will only apply to base entry if used with :delete-subtree). 305 | :proxied-auth The dn: or u: to be used as the authorization 306 | identity when processing the request. Don't forget the dn:/u: prefix. 307 | ```clojure 308 | (ldap/delete conn "cn=dude,ou=people,dc=example,dc=com") 309 | 310 | (ldap/delete conn "cn=dude,ou=people,dc=example,dc=com" 311 | {:pre-read #{:telephoneNumber}}) 312 | ``` 313 | Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) if the object does not exist or an error occurs. 314 | -------------------------------------------------------------------------------- /test-resources/in-memeory-ds-schema: -------------------------------------------------------------------------------- 1 | ; This is a dump of the InMemeoryDirectoryServer schema 2 | ({:attributeTypes 3 | ["( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 X-ORIGIN 'RFC 4512' )" 4 | "( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'RFC 4512' )" 5 | "( 2.5.18.3 NAME 'creatorsName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 6 | "( 2.5.18.1 NAME 'createTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 7 | "( 2.5.18.4 NAME 'modifiersName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 8 | "( 2.5.18.2 NAME 'modifyTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 9 | "( 2.5.21.9 NAME 'structuralObjectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 10 | "( 2.5.21.10 NAME 'governingStructureRule' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 11 | "( 2.5.18.10 NAME 'subschemaSubentry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 12 | "( 2.5.21.6 NAME 'objectClasses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 13 | "( 2.5.21.5 NAME 'attributeTypes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 14 | "( 2.5.21.4 NAME 'matchingRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 15 | "( 2.5.21.8 NAME 'matchingRuleUse' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 16 | "( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 17 | "( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 18 | "( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 19 | "( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation X-ORIGIN 'RFC 4512' )" 20 | "( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 21 | "( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 22 | "( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 23 | "( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 24 | "( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 25 | "( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 26 | "( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation X-ORIGIN 'RFC 4512' )" 27 | "( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 28 | "( 2.5.4.15 NAME 'businessCategory' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 29 | "( 2.5.4.6 NAME 'c' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE X-ORIGIN 'RFC 4519' )" 30 | "( 2.5.4.3 NAME 'cn' SUP name X-ORIGIN 'RFC 4519' )" 31 | "( 0.9.2342.19200300.100.1.25 NAME 'dc' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE X-ORIGIN 'RFC 4519' )" 32 | "( 2.5.4.13 NAME 'description' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 33 | "( 2.5.4.27 NAME 'destinationIndicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'RFC 4519' )" 34 | "( 2.5.4.49 NAME 'distinguishedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'RFC 4519' )" 35 | "( 2.5.4.46 NAME 'dnQualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'RFC 4519' )" 36 | "( 2.5.4.47 NAME 'enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 X-ORIGIN 'RFC 4519' )" 37 | "( 2.5.4.23 NAME 'facsimileTelephoneNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 X-ORIGIN 'RFC 4519' )" 38 | "( 2.5.4.44 NAME 'generationQualifier' SUP name X-ORIGIN 'RFC 4519' )" 39 | "( 2.5.4.42 NAME 'givenName' SUP name X-ORIGIN 'RFC 4519' )" 40 | "( 2.5.4.51 NAME 'houseIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 41 | "( 2.5.4.43 NAME 'initials' SUP name X-ORIGIN 'RFC 4519' )" 42 | "( 2.5.4.25 NAME 'internationalISDNNumber' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 X-ORIGIN 'RFC 4519' )" 43 | "( 2.5.4.7 NAME 'l' SUP name X-ORIGIN 'RFC 4519' )" 44 | "( 2.5.4.31 NAME 'member' SUP distinguishedName X-ORIGIN 'RFC 4519' )" 45 | "( 2.5.4.10 NAME 'o' SUP name X-ORIGIN 'RFC 4519' )" 46 | "( 2.5.4.11 NAME 'ou' SUP name X-ORIGIN 'RFC 4519' )" 47 | "( 2.5.4.32 NAME 'owner' SUP distinguishedName X-ORIGIN 'RFC 4519' )" 48 | "( 2.5.4.19 NAME 'physicalDeliveryOfficeName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 49 | "( 2.5.4.16 NAME 'postalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 X-ORIGIN 'RFC 4519' )" 50 | "( 2.5.4.17 NAME 'postalCode' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 51 | "( 2.5.4.18 NAME 'postOfficeBox' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 52 | "( 2.5.4.28 NAME 'preferredDeliveryMethod' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE X-ORIGIN 'RFC 4519' )" 53 | "( 2.5.4.26 NAME 'registeredAddress' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 X-ORIGIN 'RFC 4519' )" 54 | "( 2.5.4.33 NAME 'roleOccupant' SUP distinguishedName X-ORIGIN 'RFC 4519' )" 55 | "( 2.5.4.14 NAME 'searchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 X-ORIGIN 'RFC 4519' )" 56 | "( 2.5.4.34 NAME 'seeAlso' SUP distinguishedName X-ORIGIN 'RFC 4519' )" 57 | "( 2.5.4.5 NAME 'serialNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'RFC 4519' )" 58 | "( 2.5.4.4 NAME 'sn' SUP name X-ORIGIN 'RFC 4519' )" 59 | "( 2.5.4.8 NAME 'st' SUP name X-ORIGIN 'RFC 4519' )" 60 | "( 2.5.4.9 NAME 'street' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 61 | "( 2.5.4.20 NAME 'telephoneNumber' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 X-ORIGIN 'RFC 4519' )" 62 | "( 2.5.4.22 NAME 'teletexTerminalIdentifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 X-ORIGIN 'RFC 4519' )" 63 | "( 2.5.4.21 NAME 'telexNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 X-ORIGIN 'RFC 4519' )" 64 | "( 2.5.4.12 NAME 'title' SUP name X-ORIGIN 'RFC 4519' )" 65 | "( 0.9.2342.19200300.100.1.1 NAME 'uid' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4519' )" 66 | "( 2.5.4.50 NAME 'uniqueMember' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 X-ORIGIN 'RFC 4519' )" 67 | "( 2.5.4.35 NAME 'userPassword' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'RFC 4519' )" 68 | "( 2.5.4.24 NAME 'x121Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 X-ORIGIN 'RFC 4519' )" 69 | "( 2.5.4.45 NAME 'x500UniqueIdentifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 X-ORIGIN 'RFC 4519' )" 70 | "( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 2798' )" 71 | "( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC 'identifies a department within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 2798' )" 72 | "( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'preferred name of a person to be used when displaying entries' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'RFC 2798' )" 73 | "( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'RFC 2798' )" 74 | "( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 2798' )" 75 | "( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 X-ORIGIN 'RFC 2798' )" 76 | "( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DESC 'preferred written or spoken language for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'RFC 2798' )" 77 | "( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate' DESC 'PKCS#7 SignedData used to support S/MIME' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 X-ORIGIN 'RFC 2798' )" 78 | "( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'PKCS #12 PFX PDU for exchange of personal identity information' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 X-ORIGIN 'RFC 2798' )" 79 | "( 2.5.4.36 NAME 'userCertificate' DESC 'X.509 user certificate' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 X-ORIGIN 'RFC 4523' )" 80 | "( 2.5.4.37 NAME 'cACertificate' DESC 'X.509 CA certificate' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 X-ORIGIN 'RFC 4523' )" 81 | "( 2.5.4.40 NAME 'crossCertificatePair' DESC 'X.509 cross certificate pair' EQUALITY certificatePairExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 X-ORIGIN 'RFC 4523' )" 82 | "( 2.5.4.39 NAME 'certificateRevocationList' DESC 'X.509 certificate revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 X-ORIGIN 'RFC 4523' )" 83 | "( 2.5.4.38 NAME 'authorityRevocationList' DESC 'X.509 authority revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 X-ORIGIN 'RFC 4523' )" 84 | "( 2.5.4.53 NAME 'deltaRevocationList' DESC 'X.509 delta revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 X-ORIGIN 'RFC 4523' )" 85 | "( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'X.509 supported algorithms' EQUALITY algorithmIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 X-ORIGIN 'RFC 4523' )" 86 | "( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 X-ORIGIN 'RFC 4524' )" 87 | "( 0.9.2342.19200300.100.1.38 NAME 'associatedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'RFC 4524' )" 88 | "( 0.9.2342.19200300.100.1.48 NAME 'buildingName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 89 | "( 0.9.2342.19200300.100.1.43 NAME 'co' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4524' )" 90 | "( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'RFC 4524' )" 91 | "( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 92 | "( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 93 | "( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 4524' )" 94 | "( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 95 | "( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 96 | "( 0.9.2342.19200300.100.1.5 NAME 'drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 97 | "( 0.9.2342.19200300.100.1.20 NAME 'homePhone' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 X-ORIGIN 'RFC 4524' )" 98 | "( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 X-ORIGIN 'RFC 4524' )" 99 | "( 0.9.2342.19200300.100.1.9 NAME 'host' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 100 | "( 0.9.2342.19200300.100.1.4 NAME 'info' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} X-ORIGIN 'RFC 4524' )" 101 | "( 0.9.2342.19200300.100.1.3 NAME 'mail' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} X-ORIGIN 'RFC 4524' )" 102 | "( 0.9.2342.19200300.100.1.10 NAME 'manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'RFC 4524' )" 103 | "( 0.9.2342.19200300.100.1.41 NAME 'mobile' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 X-ORIGIN 'RFC 4524' )" 104 | "( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 105 | "( 0.9.2342.19200300.100.1.42 NAME 'pager' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 X-ORIGIN 'RFC 4524' )" 106 | "( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 107 | "( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 108 | "( 0.9.2342.19200300.100.1.21 NAME 'secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'RFC 4524' )" 109 | "( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 110 | "( 0.9.2342.19200300.100.1.8 NAME 'userClass' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} X-ORIGIN 'RFC 4524' )" 111 | "( 0.9.2342.19200300.100.1.55 NAME 'audio' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{250000} X-ORIGIN 'RFC 2798' )" 112 | "( 0.9.2342.19200300.100.1.7 NAME 'photo' X-ORIGIN 'RFC 2798' )" 113 | "( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' EQUALITY caseExactMatch SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RFC 2798' )" 114 | "( 1.3.6.1.1.20 NAME 'entryDN' DESC 'DN of the entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 5020' )" 115 | "( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'named reference - a labeledURI' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation X-ORIGIN 'RFC 3296' )" 116 | "( 1.3.6.1.1.4 NAME 'vendorName' EQUALITY 1.3.6.1.4.1.1466.109.114.1 SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'RFC 3045' )" 117 | "( 1.3.6.1.1.5 NAME 'vendorVersion' EQUALITY 1.3.6.1.4.1.1466.109.114.1 SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'RFC 3045' )" 118 | "( 1.3.6.1.1.16.4 NAME 'entryUUID' DESC 'UUID of the entry' EQUALITY uuidMatch ORDERING uuidOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'RFC 4530' )" 119 | "( 1.3.6.1.4.1.453.16.2.103 NAME 'numSubordinates' DESC 'count of immediate subordinates' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.453.16.2.103 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation X-ORIGIN 'draft-ietf-boreham-numsubordinates' )" 120 | "( 1.3.6.1.4.1.7628.5.4.1 NAME 'inheritable' SYNTAX BOOLEAN SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'draft-ietf-ldup-subentry' )" 121 | "( 1.3.6.1.4.1.7628.5.4.2 NAME 'blockInheritance' SYNTAX BOOLEAN SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation X-ORIGIN 'draft-ietf-ldup-subentry' )" 122 | "( 2.16.840.1.113730.3.1.5 NAME 'changeNumber' DESC 'a number which uniquely identifies a change made to a directory entry' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 123 | "( 2.16.840.1.113730.3.1.6 NAME 'targetDN' DESC 'the DN of the entry which was modified' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 124 | "( 2.16.840.1.113730.3.1.7 NAME 'changeType' DESC 'the type of change made to an entry' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 125 | "( 2.16.840.1.113730.3.1.8 NAME 'changes' DESC 'a set of changes to apply to an entry' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'draft-good-ldap-changelog' )" 126 | "( 2.16.840.1.113730.3.1.9 NAME 'newRDN' DESC 'the new RDN of an entry which is the target of a modrdn operation' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 127 | "( 2.16.840.1.113730.3.1.10 NAME 'deleteOldRDN' DESC 'a flag which indicates if the old RDN should be retained as an attribute of the entry' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 128 | "( 2.16.840.1.113730.3.1.11 NAME 'newSuperior' DESC 'the new parent of an entry which is the target of a moddn operation' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'draft-good-ldap-changelog' )" 129 | "( 2.16.840.1.113730.3.1.35 NAME 'changelog' DESC 'the distinguished name of the entry which contains the set of entries comprising the server changelog' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'draft-good-ldap-changelog' )" 130 | "( 1.3.6.1.4.1.4203.1.3.3 NAME 'supportedAuthPasswordSchemes' DESC 'supported password storage schemes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} USAGE dSAOperation X-ORIGIN 'RFC 3112' )" 131 | "( 1.3.6.1.4.1.4203.1.3.4 NAME 'authPassword' DESC 'password authentication information' EQUALITY 1.3.6.1.4.1.4203.1.2.2 SYNTAX 1.3.6.1.4.1.4203.1.1.2 X-ORIGIN 'RFC 3112' )" 132 | "( 2.16.840.1.113730.3.1.55 NAME 'aci' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE directoryOperation X-ORIGIN 'De facto standard' )"], 133 | :ldapSyntaxes 134 | ["( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' X-ORIGIN 'RFC 4517' )" 135 | "( 1.3.6.1.4.1.1466.115.121.1.6 DESC 'Bit String' X-ORIGIN 'RFC 4517')" 136 | "( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' X-ORIGIN 'RFC 4517')" 137 | "( 1.3.6.1.4.1.1466.115.121.1.11 DESC 'Country String' X-ORIGIN 'RFC 4517')" 138 | "( 1.3.6.1.4.1.1466.115.121.1.14 DESC 'Delivery Method' X-ORIGIN 'RFC 4517')" 139 | "( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' X-ORIGIN 'RFC 4517')" 140 | "( 1.3.6.1.4.1.1466.115.121.1.16 DESC 'DIT Content Rule Description' X-ORIGIN 'RFC 4517')" 141 | "( 1.3.6.1.4.1.1466.115.121.1.17 DESC 'DIT Structure Rule Description' X-ORIGIN 'RFC 4517')" 142 | "( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'DN' X-ORIGIN 'RFC 4517')" 143 | "( 1.3.6.1.4.1.1466.115.121.1.21 DESC 'Enhanced Guide' X-ORIGIN 'RFC 4517')" 144 | "( 1.3.6.1.4.1.1466.115.121.1.22 DESC 'Facsimile Telephone Number' X-ORIGIN 'RFC 4517')" 145 | "( 1.3.6.1.4.1.1466.115.121.1.23 DESC 'Fax' X-ORIGIN 'RFC 4517')" 146 | "( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' X-ORIGIN 'RFC 4517')" 147 | "( 1.3.6.1.4.1.1466.115.121.1.25 DESC 'Guide' X-ORIGIN 'RFC 4517')" 148 | "( 1.3.6.1.4.1.1466.115.121.1.26 DESC 'IA5 String' X-ORIGIN 'RFC 4517')" 149 | "( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'INTEGER' X-ORIGIN 'RFC 4517')" 150 | "( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-ORIGIN 'RFC 4517')" 151 | "( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' X-ORIGIN 'RFC 4517')" 152 | "( 1.3.6.1.4.1.1466.115.121.1.30 DESC 'Matching Rule Description' X-ORIGIN 'RFC 4517')" 153 | "( 1.3.6.1.4.1.1466.115.121.1.31 DESC 'Matching Rule Use Description' X-ORIGIN 'RFC 4517')" 154 | "( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' X-ORIGIN 'RFC 4517')" 155 | "( 1.3.6.1.4.1.1466.115.121.1.35 DESC 'Name Form Description' X-ORIGIN 'RFC 4517')" 156 | "( 1.3.6.1.4.1.1466.115.121.1.36 DESC 'Numeric String' X-ORIGIN 'RFC 4517')" 157 | "( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' X-ORIGIN 'RFC 4517')" 158 | "( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' X-ORIGIN 'RFC 4517')" 159 | "( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' X-ORIGIN 'RFC 4517')" 160 | "( 1.3.6.1.4.1.1466.115.121.1.39 DESC 'Other Mailbox' X-ORIGIN 'RFC 4517')" 161 | "( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' X-ORIGIN 'RFC 4517')" 162 | "( 1.3.6.1.4.1.1466.115.121.1.44 DESC 'Printable String' X-ORIGIN 'RFC 4517')" 163 | "( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' X-ORIGIN 'RFC 4517')" 164 | "( 1.3.6.1.4.1.1466.115.121.1.50 DESC 'Telephone Number' X-ORIGIN 'RFC 4517')" 165 | "( 1.3.6.1.4.1.1466.115.121.1.51 DESC 'Teletex Terminal Identifier' X-ORIGIN 'RFC 4517')" 166 | "( 1.3.6.1.4.1.1466.115.121.1.52 DESC 'Telex Number' X-ORIGIN 'RFC 4517')" 167 | "( 1.3.6.1.4.1.1466.115.121.1.53 DESC 'UTC Time' X-ORIGIN 'RFC 4517')"], 168 | :objectClasses 169 | ["( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass X-ORIGIN 'RFC 4512' )" 170 | "( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName X-ORIGIN 'RFC 4512' )" 171 | "( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' SUP top AUXILIARY X-ORIGIN 'RFC 4512' )" 172 | "( 2.5.20.1 NAME 'subschema' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) X-ORIGIN 'RFC 4512' )" 173 | "( 2.5.6.11 NAME 'applicationProcess' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) X-ORIGIN 'RFC 4519' )" 174 | "( 2.5.6.2 NAME 'country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) X-ORIGIN 'RFC 4519' )" 175 | "( 1.3.6.1.4.1.1466.344 NAME 'dcObject' SUP top AUXILIARY MUST dc X-ORIGIN 'RFC 4519' )" 176 | "( 2.5.6.14 NAME 'device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) X-ORIGIN 'RFC 4519' )" 177 | "( 2.5.6.9 NAME 'groupOfNames' SUP top STRUCTURAL MUST cn MAY ( member $ businessCategory $ seeAlso $ owner $ ou $ o $ description ) X-ORIGIN 'RFC 4519' )" 178 | "( 2.5.6.17 NAME 'groupOfUniqueNames' SUP top STRUCTURAL MUST cn MAY ( uniqueMember $ businessCategory $ seeAlso $ owner $ ou $ o $ description ) X-ORIGIN 'RFC 4519' )" 179 | "( 2.5.6.3 NAME 'locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) X-ORIGIN 'RFC 4519' )" 180 | "( 2.5.6.4 NAME 'organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) X-ORIGIN 'RFC 4519' )" 181 | "( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) X-ORIGIN 'RFC 4519' )" 182 | "( 2.5.6.7 NAME 'organizationalPerson' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) X-ORIGIN 'RFC 4519' )" 183 | "( 2.5.6.8 NAME 'organizationalRole' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) X-ORIGIN 'RFC 4519' )" 184 | "( 2.5.6.5 NAME 'organizationalUnit' SUP top STRUCTURAL MUST ou MAY ( businessCategory $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationalISDNNumber $ l $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ searchGuide $ seeAlso $ st $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ userPassword $ x121Address ) X-ORIGIN 'RFC 4519' )" 185 | "( 2.5.6.10 NAME 'residentialPerson' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) X-ORIGIN 'RFC 4519' )" 186 | "( 1.3.6.1.1.3.1 NAME 'uidObject' SUP top AUXILIARY MUST uid X-ORIGIN 'RFC 4519' )" 187 | "( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) X-ORIGIN 'RFC 2798' )" 188 | "( 2.5.6.21 NAME 'pkiUser' DESC 'X.509 PKI User' SUP top AUXILIARY MAY userCertificate X-ORIGIN 'RFC 4523' )" 189 | "( 2.5.6.22 NAME 'pkiCA' DESC 'X.509 PKI Certificate Authority' SUP top AUXILIARY MAY ( cACertificate $ certificateRevocationList $ authorityRevocationList $ crossCertificatePair ) X-ORIGIN 'RFC 4523' )" 190 | "( 2.5.6.19 NAME 'cRLDistributionPoint' DESC 'X.509 CRL distribution point' SUP top STRUCTURAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) X-ORIGIN 'RFC 4523' )" 191 | "( 2.5.6.23 NAME 'deltaCRL' DESC 'X.509 delta CRL' SUP top AUXILIARY MAY deltaRevocationList X-ORIGIN 'RFC 4523' )" 192 | "( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'X.521 strong authentication user' SUP top AUXILIARY MUST userCertificate X-ORIGIN 'RFC 4523' )" 193 | "( 2.5.6.18 NAME 'userSecurityInformation' DESC 'X.521 user security information' SUP top AUXILIARY MAY ( supportedAlgorithms ) X-ORIGIN 'RFC 4523' )" 194 | "( 2.5.6.16 NAME 'certificationAuthority' DESC 'X.509 certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair X-ORIGIN 'RFC 4523' )" 195 | "( 2.5.6.16.2 NAME 'certificationAuthority-V2' DESC 'X.509 certificate authority, version 2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList X-ORIGIN 'RFC 4523' )" 196 | "( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST uid MAY ( description $ seeAlso $ l $ o $ ou $ host ) X-ORIGIN 'RFC 4524' )" 197 | "( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( cn $ description $ seeAlso $ l $ o $ ou $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) X-ORIGIN 'RFC 4524' )" 198 | "( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST cn MAY ( description $ l $ o $ ou $ seeAlso $ telephonenumber ) X-ORIGIN 'RFC 4524' )" 199 | "( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST dc MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description $ o $ associatedName ) X-ORIGIN 'RFC 4524' )" 200 | "( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' SUP top AUXILIARY MUST associatedDomain X-ORIGIN 'RFC 4524' )" 201 | "( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST co X-ORIGIN 'RFC 4524' )" 202 | "( 0.9.2342.19200300.100.4.14 NAME 'rFC822localPart' SUP domain STRUCTURAL MAY ( cn $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationaliSDNNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ seeAlso $ sn $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ x121Address ) X-ORIGIN 'RFC 4524' )" 203 | "( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST cn MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) X-ORIGIN 'RFC 4524' )" 204 | "( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' SUP top AUXILIARY MUST userPassword X-ORIGIN 'RFC 4524' )" 205 | "( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'named subordinate reference object' STRUCTURAL MUST ref X-ORIGIN 'RFC 3296' )" 206 | "( 1.3.6.1.4.1.5322.13.1.1 NAME 'namedObject' SUP top STRUCTURAL MAY cn X-ORIGIN 'draft-howard-namedobject' )" 207 | "( 2.16.840.1.113719.2.142.6.1.1 NAME 'ldapSubEntry' DESC 'LDAP Subentry class, version 1' SUP top STRUCTURAL MAY ( cn ) X-ORIGIN 'draft-ietf-ldup-subentry' )" 208 | "( 1.3.6.1.4.1.7628.5.6.1.1 NAME 'inheritableLDAPSubEntry' DESC 'Inheritable LDAP Subentry class, version 1' SUP ldapSubEntry STRUCTURAL MUST ( inheritable ) MAY ( blockInheritance ) X-ORIGIN 'draft-ietf-ldup-subentry' )" 209 | "( 2.16.840.1.113730.3.2.1 NAME 'changeLogEntry' SUP top STRUCTURAL MUST ( changeNumber $ targetDN $ changeType ) MAY ( changes $ newRDN $ deleteOldRDN $ newSuperior ) X-ORIGIN 'draft-good-ldap-changelog' )" 210 | "( 1.3.6.1.4.1.4203.1.4.7 NAME 'authPasswordObject' DESC 'authentication password mix in class' AUXILIARY MAY authPassword X-ORIGIN 'RFC 3112' )"], 211 | :dn "cn=schema"}) 212 | -------------------------------------------------------------------------------- /src/clj_ldap/client.clj: -------------------------------------------------------------------------------- 1 | (ns clj-ldap.client 2 | "LDAP client" 3 | (:refer-clojure :exclude [get]) 4 | (:require [clojure.string :as string] 5 | [clojure.pprint :refer (pprint)]) 6 | (:import [com.unboundid.ldap.sdk 7 | LDAPResult 8 | LDAPConnectionOptions 9 | LDAPConnection 10 | ResultCode 11 | LDAPConnectionPool 12 | LDAPException 13 | Attribute 14 | Entry 15 | ModificationType 16 | ModifyRequest 17 | ModifyDNRequest 18 | Modification 19 | DeleteRequest 20 | SimpleBindRequest 21 | RoundRobinServerSet 22 | SearchRequest 23 | LDAPEntrySource 24 | EntrySourceException 25 | SearchScope 26 | DereferencePolicy 27 | LDAPSearchException 28 | Control 29 | StartTLSPostConnectProcessor 30 | CompareRequest 31 | CompareResult BindResult]) 32 | (:import [com.unboundid.ldap.sdk.extensions 33 | PasswordModifyExtendedRequest 34 | PasswordModifyExtendedResult 35 | WhoAmIExtendedRequest 36 | WhoAmIExtendedResult 37 | StartTLSExtendedRequest]) 38 | (:import [com.unboundid.ldap.sdk.controls 39 | PreReadRequestControl 40 | PostReadRequestControl 41 | PreReadResponseControl 42 | PostReadResponseControl 43 | ProxiedAuthorizationV2RequestControl 44 | SimplePagedResultsControl 45 | ServerSideSortRequestControl 46 | SortKey 47 | SubtreeDeleteRequestControl]) 48 | (:import [com.unboundid.util 49 | Base64]) 50 | (:import [com.unboundid.util.ssl 51 | SSLUtil 52 | TrustAllTrustManager 53 | TrustStoreTrustManager])) 54 | 55 | ;;======== Helper functions ==================================================== 56 | 57 | (def not-nil? (complement nil?)) 58 | 59 | ;; Define get-value to return String or Bytes depending on boolean 'byte-value' 60 | (defmulti ^:private get-value (fn [attr byte-value] byte-value)) 61 | 62 | (defmethod ^:private get-value true 63 | [attr _] 64 | (if (> (.size attr) 1) 65 | (vec (.getValueByteArrays attr)) 66 | (.getValueByteArray attr))) 67 | 68 | (defmethod ^:private get-value false 69 | [attr _] 70 | (if (> (.size attr) 1) 71 | (vec (.getValues attr)) 72 | (.getValue attr))) 73 | 74 | (defn- encodeBase64 [attr] 75 | "Can be used to produce LDIF" 76 | (if (> (.size attr) 1) 77 | (map #(Base64/encode %) (get-value attr)) 78 | (Base64/encode (get-value attr)))) 79 | 80 | (defn- extract-attribute 81 | "Extracts [:name value] from the given attribute object. Converts 82 | the objectClass attribute to a set. The byte-valued collection is 83 | referenced to detemine whether to return a String or byte array" 84 | [attr byte-valued] 85 | (let [k (keyword (.getName attr)) 86 | byte-value (contains? (set byte-valued) (keyword (.getName attr)))] 87 | (cond 88 | (= :objectClass k) [k (set (get-value attr byte-value))] 89 | :else [k (get-value attr byte-value)]))) 90 | 91 | (defn- entry-as-map 92 | "Returns a closure which converts an Entry object into a map optionally 93 | adding the DN. We pass along the byte-valued collection to properly 94 | return binary data." 95 | ([byte-valued] 96 | (entry-as-map byte-valued true)) 97 | ([byte-valued dn?] 98 | (fn [entry] 99 | (let [attrs (seq (.getAttributes entry))] 100 | (if dn? 101 | (apply hash-map :dn (.getDN entry) 102 | (mapcat #(extract-attribute % byte-valued) attrs)) 103 | (apply hash-map 104 | (mapcat #(extract-attribute % byte-valued) attrs))))))) 105 | 106 | (defn- add-response-control 107 | "Adds the values contained in given response control to the given map" 108 | [m control] 109 | (condp instance? control 110 | PreReadResponseControl 111 | (update-in m [:pre-read] merge ((entry-as-map [] false) 112 | (.getEntry control))) 113 | PostReadResponseControl 114 | (update-in m [:post-read] merge ((entry-as-map [] false) 115 | (.getEntry control))) 116 | m)) 117 | 118 | (defn- add-response-controls 119 | "Adds the values contained in the given response controls to the given map" 120 | [controls m] 121 | (reduce add-response-control m (seq controls))) 122 | 123 | (defn- ldap-result 124 | "Converts an LDAPResult object into a map" 125 | [obj] 126 | (let [res (.getResultCode obj) 127 | controls (.getResponseControls obj)] 128 | (add-response-controls 129 | controls 130 | {:code (.intValue res) 131 | :name (.getName res)}))) 132 | 133 | (defn- connection-options 134 | "Returns a LDAPConnectionOptions object" 135 | [{:keys [connect-timeout timeout]}] 136 | (let [opt (LDAPConnectionOptions.)] 137 | (when connect-timeout (.setConnectTimeoutMillis opt connect-timeout)) 138 | (when timeout (.setResponseTimeoutMillis opt timeout)) 139 | opt)) 140 | 141 | (defn- create-ssl-context 142 | "Returns a SSLContext object" 143 | [{:keys [trust-store]}] 144 | (let [trust-manager (if trust-store 145 | (TrustStoreTrustManager. trust-store) 146 | (TrustAllTrustManager.)) 147 | ssl-util (SSLUtil. trust-manager)] 148 | (.createSSLContext ssl-util))) 149 | 150 | (defn- create-ssl-factory 151 | "Returns a SSLSocketFactory object" 152 | [{:keys [trust-store]}] 153 | (let [trust-manager (if trust-store 154 | (TrustStoreTrustManager. trust-store) 155 | (TrustAllTrustManager.)) 156 | ssl-util (SSLUtil. trust-manager)] 157 | (.createSSLSocketFactory ssl-util))) 158 | 159 | (defn- host-as-map 160 | "Returns a single host as a map containing an :address and an optional 161 | :port" 162 | [host] 163 | (cond 164 | (nil? host) {:address "localhost" :port 389} 165 | (string? host) (let [[address port] (string/split host #":")] 166 | {:address (if (= address "") 167 | "localhost" 168 | address) 169 | :port (if port 170 | (int (Integer. port)))}) 171 | (map? host) (merge {:address "localhost"} host) 172 | :else (throw 173 | (IllegalArgumentException. 174 | (str "Invalid host for an ldap connection : " 175 | host))))) 176 | 177 | (defn- create-connection 178 | "Create an LDAPConnection object" 179 | [{:keys [host ssl? startTLS?] 180 | :as options 181 | :or {ssl? false startTLS? false}}] 182 | (let [h (host-as-map host) 183 | host (:address h) 184 | ldap-port (or (:port h) 389) 185 | ldaps-port (or (:port h) 636) 186 | opt (connection-options options)] 187 | (cond 188 | ssl? (let [ssl (create-ssl-factory options)] 189 | (LDAPConnection. ssl opt host ldaps-port)) 190 | startTLS? (let [conn (LDAPConnection. opt host ldap-port)] 191 | (.processExtendedOperation conn (StartTLSExtendedRequest. (create-ssl-context options))) 192 | conn) 193 | :else (LDAPConnection. opt host ldap-port)))) 194 | 195 | (defn- bind-based-on-connection 196 | "Common bind approach for the api: 197 | connection represents pool then authenticate and revert bind association on pool connection, or 198 | connection is plain then authenticate and remain bound. 199 | Note: There is a retainIdentity control (1.3.6.1.4.1.30221.2.5.3) which might also be useful option in the plain 200 | connection context but since we make this the default behavior of pool binds it is likely unnecessary." 201 | [connection bind-dn password] 202 | (if (instance? LDAPConnectionPool connection) 203 | (.bindAndRevertAuthentication connection bind-dn password nil) 204 | (.bind connection bind-dn password))) 205 | 206 | (defn- bind-request 207 | "Returns a BindRequest object" 208 | [{:keys [bind-dn password]}] 209 | (if bind-dn 210 | (SimpleBindRequest. bind-dn password) 211 | (SimpleBindRequest.))) 212 | 213 | (defn- create-server-set 214 | "Returns a RoundRobinServerSet" 215 | [{:keys [host ssl? startTLS?] 216 | :as options 217 | :or {ssl? false startTLS? false}}] 218 | (let [hosts (map host-as-map host) 219 | addresses (into-array (map :address hosts)) 220 | opt (connection-options options)] 221 | (if ssl? 222 | (let [ssl (create-ssl-factory options) 223 | ports (int-array (map #(or (:port %) (int 636)) hosts))] 224 | (RoundRobinServerSet. addresses ports ssl opt)) 225 | (let [ports (int-array (map #(or (:port %) (int 389)) hosts))] 226 | (RoundRobinServerSet. addresses ports opt))))) 227 | 228 | (defn- connect-to-host 229 | "Connect to a single host" 230 | [{:keys [num-connections initial-connections max-connections startTLS?] 231 | :as options 232 | :or {num-connections 1 startTLS? false}}] 233 | (let [connection (create-connection options) 234 | bind-result (.bind connection (bind-request options)) 235 | pcp (if startTLS? 236 | (StartTLSPostConnectProcessor. (create-ssl-context options)) 237 | nil) 238 | initial-connections (or initial-connections num-connections) 239 | max-connections (or max-connections initial-connections)] 240 | (if (= ResultCode/SUCCESS (.getResultCode bind-result)) 241 | (LDAPConnectionPool. connection initial-connections max-connections pcp) 242 | (throw (LDAPException. bind-result))))) 243 | 244 | (defn- connect-to-hosts 245 | "Connects to multiple hosts" 246 | [{:keys [num-connections initial-connections max-connections startTLS?] 247 | :as options 248 | :or {num-connections 1 startTLS? false}}] 249 | (let [server-set (create-server-set options) 250 | bind-request (bind-request options) 251 | pcp (when startTLS? 252 | (StartTLSPostConnectProcessor. (create-ssl-context options))) 253 | initial-connections (or initial-connections num-connections) 254 | max-connections (or max-connections initial-connections)] 255 | (LDAPConnectionPool. server-set bind-request 256 | initial-connections max-connections pcp))) 257 | 258 | 259 | (defn- set-entry-kv! 260 | "Sets the given key/value pair in the given entry object" 261 | [entry-obj k v] 262 | (let [name-str (name k)] 263 | (.addAttribute entry-obj 264 | (if (coll? v) 265 | (Attribute. name-str (into-array v)) 266 | (Attribute. name-str (str v)))))) 267 | 268 | (defn- set-entry-map! 269 | "Sets the attributes in the given entry object using the given map" 270 | [entry-obj m] 271 | (doseq [[k v] m] 272 | (set-entry-kv! entry-obj k v))) 273 | 274 | (defn- byte-array? 275 | [v] 276 | (= (type v) (type (byte-array 0)))) 277 | 278 | (defn- create-modification 279 | "Creates a modification object" 280 | [modify-op attribute values] 281 | (cond 282 | (and (coll? values) (byte-array? (first values))) 283 | (Modification. modify-op attribute (into-array values)) 284 | (coll? values) 285 | (Modification. modify-op attribute (into-array String (map str values))) 286 | (= :all values) 287 | (Modification. modify-op attribute) 288 | (byte-array? values) 289 | (Modification. modify-op attribute values) 290 | :else 291 | (Modification. modify-op attribute (str values)))) 292 | 293 | (defn- modify-ops 294 | "Returns a sequence of Modification objects to do the given operation 295 | using the contents of the given map." 296 | [modify-op modify-map] 297 | (for [[k v] modify-map] 298 | (create-modification modify-op (name k) v))) 299 | 300 | (defn- add-request-controls 301 | [request options] 302 | "Adds LDAP controls to the given request" 303 | (when (contains? options :pre-read) 304 | (let [attributes (map name (options :pre-read)) 305 | pre-read-control (PreReadRequestControl. (into-array attributes))] 306 | (.addControl request pre-read-control))) 307 | (when (contains? options :post-read) 308 | (let [attributes (map name (options :post-read)) 309 | pre-read-control (PostReadRequestControl. (into-array attributes))] 310 | (.addControl request pre-read-control))) 311 | (when (contains? options :proxied-auth) 312 | (.addControl request (ProxiedAuthorizationV2RequestControl. 313 | (:proxied-auth options)))) 314 | (when (and (contains? options :delete-subtree) (= (type request) DeleteRequest)) 315 | (.addControl request (SubtreeDeleteRequestControl.)))) 316 | 317 | (defn- get-modify-request 318 | "Sets up a ModifyRequest object using the contents of the given map" 319 | [dn modifications] 320 | (let [adds (modify-ops ModificationType/ADD (modifications :add)) 321 | deletes (modify-ops ModificationType/DELETE (modifications :delete)) 322 | replacements (modify-ops ModificationType/REPLACE 323 | (modifications :replace)) 324 | increments (modify-ops ModificationType/INCREMENT 325 | (modifications :increment)) 326 | all (concat adds deletes replacements increments)] 327 | (doto (ModifyRequest. dn (into-array all)) 328 | (add-request-controls modifications)))) 329 | 330 | (defn- next-entry 331 | "Attempts to get the next entry from an LDAPEntrySource object" 332 | [source] 333 | (try 334 | (.nextEntry source) 335 | (catch EntrySourceException e 336 | (if (.mayContinueReading e) 337 | (.nextEntry source) 338 | (throw e))))) 339 | 340 | (defn- entry-seq 341 | "Returns a lazy sequence of entries from an LDAPEntrySource object" 342 | [source] 343 | (if-let [n (.nextEntry source)] 344 | (cons n (lazy-seq (entry-seq source))))) 345 | 346 | ;; Extended version of search-results function using a 347 | ;; SearchRequest that uses a SimplePagedResultsControl. 348 | ;; Allows us to read arbitrarily large result sets. 349 | (defn- search-all-results 350 | "Returns a lazy sequence of search results via paging so we don't run into 351 | size limits with the number of results." 352 | ([connection {:keys [base scope filter attributes size-limit time-limit 353 | types-only controls byte-valued 354 | cookie page-size request] 355 | :or {page-size 500} 356 | :as options}] 357 | (let [paging (SimplePagedResultsControl. page-size cookie) 358 | req (doto (or request 359 | (cond-> (SearchRequest. base scope DereferencePolicy/NEVER 360 | size-limit time-limit types-only filter attributes) 361 | (seq controls) (doto (.addControls (into-array Control controls))))) 362 | (.setControls (list paging))) 363 | response (.search connection req) 364 | results (->> response 365 | .getSearchEntries 366 | (map (entry-as-map byte-valued)) 367 | (remove empty?)) 368 | next-paging (SimplePagedResultsControl/get response) 369 | next-cookie (.getCookie next-paging)] 370 | (if (and next-paging (pos? (.getValueLength next-cookie))) 371 | (search-all-results connection 372 | (merge options {:cookie next-cookie :request request}) 373 | results) 374 | results))) 375 | ([connection options results] 376 | (lazy-seq (concat results (search-all-results connection options))))) 377 | 378 | (defn- search-results 379 | "Returns a sequence of search results for the given search criteria. 380 | Ignore a size limit exceeded exception if one occurs. If the caller 381 | provided a respf then apply the function to any response controls." 382 | [conn {:keys [base scope filter attributes size-limit time-limit types-only 383 | controls respf byte-valued]}] 384 | (try 385 | (let [req (SearchRequest. base scope DereferencePolicy/NEVER size-limit 386 | time-limit types-only filter attributes) 387 | - (and (not (empty? controls)) 388 | (.addControls req (into-array Control controls))) 389 | res (.search conn req)] 390 | (when (not-nil? respf) (respf (.getResponseControls res))) 391 | (if (> (.getEntryCount res) 0) 392 | (map (entry-as-map byte-valued) (.getSearchEntries res)))) 393 | (catch LDAPSearchException e 394 | (when (not-nil? respf) (respf (.getResponseControls e))) 395 | (if (= ResultCode/SIZE_LIMIT_EXCEEDED (.getResultCode e)) 396 | (map (entry-as-map byte-valued) (.getSearchEntries e)) 397 | (throw e))))) 398 | 399 | (defn- search-results! 400 | "Call the given function with the results of the search using 401 | the given search criteria" 402 | [pool {:keys [base scope filter attributes size-limit time-limit types-only 403 | controls respf byte-valued]} f] 404 | (let [req (SearchRequest. base scope DereferencePolicy/NEVER 405 | size-limit time-limit types-only filter attributes) 406 | - (and (not (empty? controls)) 407 | (.addControls req (into-array Control controls))) 408 | conn (.getConnection pool)] 409 | (try 410 | (with-open [source (LDAPEntrySource. conn req false)] 411 | (let [res (.getSearchResult source)] 412 | (when (not-nil? respf) (respf (.getResponseControls res))) 413 | (doseq [i (remove empty? 414 | (map (entry-as-map byte-valued) 415 | (entry-seq source)))] 416 | (f i)))) 417 | (.releaseConnection pool conn) 418 | (catch EntrySourceException e 419 | (.releaseDefunctConnection pool conn) 420 | (throw e))))) 421 | 422 | (defn- get-scope 423 | "Converts a keyword into a SearchScope object" 424 | [k] 425 | (condp = k 426 | :base SearchScope/BASE 427 | :one SearchScope/ONE 428 | :subordinate SearchScope/SUBORDINATE_SUBTREE 429 | SearchScope/SUB)) 430 | 431 | (defn- get-attributes 432 | "Converts a collection of attributes into an array" 433 | [attrs] 434 | (cond 435 | (or (nil? attrs) 436 | (empty? attrs)) (into-array java.lang.String 437 | [SearchRequest/ALL_USER_ATTRIBUTES]) 438 | :else (into-array java.lang.String 439 | (map name attrs)))) 440 | 441 | (defn- sortOrder 442 | "Convert friendly name to boolean according to ServerSideSort control" 443 | [order] 444 | (condp = order 445 | :ascending false 446 | :descending true 447 | :else false)) 448 | 449 | (defn- createServerSideSort 450 | "Create a ServerSideSortRequestControl base on the provided map with 451 | keys :is-critical and :sort-keys. The former is boolean valued while the latter 452 | is a vector: [:attr1 :ascending :attr2 :descending ... ]. 453 | Throw an exception if no sortKey is defined. If :is-critical is not defined, 454 | assume false." 455 | [{:keys [is-critical sort-keys] :or { is-critical false sort-keys []}}] 456 | (if (not (empty? sort-keys)) 457 | (let [keylist (map (fn [[k v]] (SortKey. (name k) (sortOrder v))) 458 | (apply array-map sort-keys))] 459 | (ServerSideSortRequestControl. is-critical (into-array SortKey keylist))) 460 | (throw (Exception. "Error: The search option 'server-sort' requires 461 | non-empty sort-keys")))) 462 | 463 | (defn- search-criteria 464 | "Given a map of search criteria and possibly other keys, return the same map 465 | with search criteria keys rewritten ready for passing to search functions." 466 | [base {:keys [scope filter attributes size-limit time-limit types-only 467 | proxied-auth controls respf server-sort byte-valued] 468 | :as original 469 | :or {size-limit 0 time-limit 0 types-only false byte-valued [] 470 | filter "(objectclass=*)" controls [] respf nil 471 | proxied-auth nil server-sort nil}}] 472 | (let [server-sort-control (if (not-nil? server-sort) 473 | [(createServerSideSort server-sort)] 474 | []) 475 | proxied-auth-control (if (not-nil? proxied-auth) 476 | [(ProxiedAuthorizationV2RequestControl. proxied-auth)] 477 | [])] 478 | (merge original {:base base 479 | :scope (get-scope scope) 480 | :filter filter 481 | :attributes (get-attributes attributes) 482 | :size-limit size-limit :time-limit time-limit 483 | :types-only types-only 484 | :controls (-> controls 485 | (into server-sort-control) 486 | (into proxied-auth-control))}))) 487 | 488 | ;;=========== API ============================================================== 489 | 490 | (defn connect 491 | "Connects to an ldap server and returns a thread-safe LDAPConnectionPool. 492 | Options is a map with the following entries: 493 | :host Either a string in the form \"address:port\" 494 | OR a map containing the keys, 495 | :address defaults to localhost 496 | :port defaults to 389 (or 636 for ldaps), 497 | OR a collection containing multiple hosts used for load 498 | balancing and failover. This entry is optional. 499 | :bind-dn The DN to bind as, optional 500 | :password The password to bind with, optional 501 | :num-connections Establish a fixed size connection pool. Defaults to 1. 502 | :initial-connections Establish a connection pool initially of this size with 503 | capability to grow to :max-connections. Defaults to 1. 504 | :max-connections Define maximum size of connection pool. It must be 505 | greater than or equal to the initial number of 506 | connections, defaults to value of :initial-connections. 507 | :ssl? Boolean, connect over SSL (ldaps), defaults to false 508 | :startTLS? Boolean, use startTLS over non-SSL port, defaults to false 509 | :trust-store Only trust SSL certificates that are in this 510 | JKS format file, optional, defaults to trusting all 511 | certificates 512 | :connect-timeout The timeout for making connections (milliseconds), 513 | defaults to 1 minute 514 | :timeout The timeout when waiting for a response from the server 515 | (milliseconds), defaults to 5 minutes 516 | " 517 | [options] 518 | (let [host (options :host)] 519 | (if (and (coll? host) 520 | (not (map? host))) 521 | (connect-to-hosts options) 522 | (connect-to-host options)))) 523 | 524 | (defn get-connection 525 | "Returns a connection from the LDAPConnectionPool object. This approach is 526 | only needed when a sequence of operations must be performed on a single 527 | connection. For example: get-connection, bind?, modify (as the bound user). 528 | The connection should be released back to the pool after use." 529 | [pool] 530 | (.getConnection pool)) 531 | 532 | (defn release-connection 533 | "Returns the original connection pool with the provided connection released 534 | and reauthenticated." 535 | [pool connection] 536 | (.releaseAndReAuthenticateConnection pool connection)) 537 | 538 | (defn bind 539 | "Performs a bind operation using the provided connection or pool, bindDN and 540 | password. If the bind is unsuccessful LDAPException is thrown. Otherwise, a 541 | map is returned with :code, :name, and optional :diagnostic-message keys. 542 | The :diagnostic-message might contain password expiration warnings, for instance. 543 | 544 | When an LDAP connection object is used as the connection argument the 545 | bind function will attempt to change the identity of that connection 546 | to that of the provided DN. Subsequent operations on that connection 547 | will be done using the bound identity. 548 | 549 | If an LDAP connection pool object is passed as the connection argument 550 | the bind attempt will have no side-effects, leaving the state of the 551 | underlying connections unchanged." 552 | [connection bind-dn password] 553 | (let [^BindResult r (bind-based-on-connection connection bind-dn password)] 554 | (merge (ldap-result r) 555 | (when-let [diagnostic-message (.getDiagnosticMessage r)] 556 | {:diagnostic-message diagnostic-message})))) 557 | 558 | (defn bind? 559 | "Performs a bind operation using the provided connection, bindDN and 560 | password. Returns true if successful and false otherwise. 561 | 562 | When an LDAP connection object is used as the connection argument the 563 | bind? function will attempt to change the identity of that connection 564 | to that of the provided DN. Subsequent operations on that connection 565 | will be done using the bound identity. 566 | 567 | If an LDAP connection pool object is passed as the connection argument 568 | the bind attempt will have no side-effects, leaving the state of the 569 | underlying connections unchanged." 570 | [connection bind-dn password] 571 | (try 572 | (let [r (bind-based-on-connection connection bind-dn password)] 573 | (= ResultCode/SUCCESS (.getResultCode r))) 574 | (catch Exception _ false))) 575 | 576 | (defn close 577 | "closes the supplied connection or pool object" 578 | [conn] 579 | (.close conn)) 580 | 581 | (defn who-am-i 582 | "Return the authorization identity associated with this connection." 583 | [connection] 584 | (let [^WhoAmIExtendedResult res (.processExtendedOperation 585 | connection (WhoAmIExtendedRequest.))] 586 | (if (= ResultCode/SUCCESS (.getResultCode res)) 587 | (let [authz-id (.getAuthorizationID res)] 588 | (cond 589 | (or (= authz-id "") (= authz-id "dn:")) "" 590 | (.startsWith authz-id "dn:") (subs authz-id 3) 591 | (.startsWith authz-id "u:") (subs authz-id 2) 592 | :else authz-id))))) 593 | 594 | (defn get 595 | "If successful, returns a map containing the entry for the given DN. 596 | Returns nil if the entry doesn't exist or cannot be read. Takes an 597 | optional collection that specifies which attributes will be returned 598 | from the server." 599 | ([connection dn] 600 | (get connection dn nil)) 601 | ([connection dn attributes] 602 | (get connection dn attributes [])) 603 | ([connection dn attributes byte-valued] 604 | (if-let [result (if attributes 605 | (.getEntry connection dn 606 | (into-array java.lang.String 607 | (map name attributes))) 608 | (.getEntry connection dn))] 609 | ((entry-as-map byte-valued) result)))) 610 | 611 | (defn add 612 | "Adds an entry to the connected ldap server. The entry is assumed to be 613 | a map. The options map supports control :proxied-auth." 614 | ([connection dn entry] 615 | (add connection dn entry nil)) 616 | ([connection dn entry options] 617 | (let [entry-obj (Entry. dn)] 618 | (set-entry-map! entry-obj entry) 619 | (when options 620 | (add-request-controls entry-obj options)) 621 | (ldap-result 622 | (.add connection entry-obj))))) 623 | 624 | (defn compare? 625 | "Determine whether the specified entry contains a given attribute value. 626 | The options map supports control :proxied-auth." 627 | ([connection dn attribute assertion-value] 628 | (compare? connection dn attribute assertion-value nil)) 629 | ([connection dn attribute assertion-value options] 630 | (let [request (CompareRequest. dn (name attribute) assertion-value)] 631 | (when (and options (:proxied-auth options)) 632 | (.addControl request (ProxiedAuthorizationV2RequestControl. 633 | (:proxied-auth options)))) 634 | (.compareMatched (.compare connection request))))) 635 | 636 | (defn modify 637 | "Modifies an entry in the connected ldap server. The modifications are 638 | a map in the form: 639 | {:add 640 | {:attribute-a some-value 641 | :attribute-b [value1 value2]} 642 | :delete 643 | {:attribute-c :all 644 | :attribute-d some-value 645 | :attribute-e [value1 value2]} 646 | :replace 647 | {:attibute-d value 648 | :attribute-e [value1 value2]} 649 | :increment 650 | {:attribute-f value} 651 | :pre-read 652 | #{:attribute-a :attribute-b} 653 | :post-read 654 | #{:attribute-c :attribute-d}} 655 | 656 | Where :add adds an attribute value, :delete deletes an attribute value and 657 | :replace replaces the set of values for the attribute with the ones specified. 658 | The entries :pre-read and :post-read specify attributes that have be read and 659 | returned either before or after the modifications have taken place." 660 | ([connection dn modifications] 661 | (modify connection dn modifications nil)) 662 | ([connection dn modifications options] 663 | (let [modify-obj (get-modify-request dn modifications)] 664 | (when options 665 | (add-request-controls modify-obj options)) 666 | (ldap-result 667 | (.modify connection modify-obj))))) 668 | 669 | (defn modify-password 670 | "Creates a new password modify extended request that will attempt to change 671 | the password of the currently-authenticated user, or another user if their 672 | DN is provided and the caller has the required authorisation." 673 | ([connection new] 674 | (let [request (PasswordModifyExtendedRequest. new)] 675 | (.processExtendedOperation connection request))) 676 | 677 | ([connection old new] 678 | (let [request (PasswordModifyExtendedRequest. old new)] 679 | (.processExtendedOperation connection request))) 680 | 681 | ([connection old new dn] 682 | (let [request (PasswordModifyExtendedRequest. dn old new)] 683 | (.processExtendedOperation connection request)))) 684 | 685 | (defn modify-rdn 686 | "Modifies the RDN (Relative Distinguished Name) of an entry in the connected 687 | ldap server. 688 | 689 | The new-rdn has the form cn=foo or ou=foo. Using just foo is not sufficient. 690 | The delete-old-rdn boolean option indicates whether to delete the current 691 | RDN value from the target entry. The options map supports pre/post-read 692 | and proxied-auth controls." 693 | ([connection dn new-rdn delete-old-rdn] 694 | (modify-rdn connection dn new-rdn delete-old-rdn nil)) 695 | ([connection dn new-rdn delete-old-rdn options] 696 | (let [request (ModifyDNRequest. dn new-rdn delete-old-rdn)] 697 | (when options 698 | (add-request-controls request options)) 699 | (ldap-result 700 | (.modifyDN connection request))))) 701 | 702 | (defn delete 703 | "Deletes the given entry in the connected ldap server. Optionally takes 704 | a map that can contain: 705 | :pre-read A set of attributes that should be read before deletion 706 | (only applied to base entry if used with :delete-subtree) 707 | :proxied-auth The dn: or u: to be used as the authorization 708 | identity when processing the request. 709 | :delete-subtree If truthy, deletes the entire subtree of DN (server must 710 | support Subtree Delete Control, 1.2.840.113556.1.4.805)" 711 | ([connection dn] 712 | (delete connection dn nil)) 713 | ([connection dn options] 714 | (let [delete-obj (DeleteRequest. dn)] 715 | (when options 716 | (add-request-controls delete-obj options)) 717 | (ldap-result 718 | (.delete connection delete-obj))))) 719 | 720 | ;; For the following search functions. 721 | ;; Options is a map with the following optional entries: 722 | ;; :scope The search scope, can be :base :one :sub or :subordinate, 723 | ;; defaults to :sub 724 | ;; :filter A string representing the search filter, 725 | ;; defaults to "(objectclass=*)" 726 | ;; :attributes A collection of the attributes to return, 727 | ;; defaults to all user attributes 728 | ;; :byte-valued A collection of attributes to return as byte arrays as 729 | ;; opposed to Strings. 730 | ;; :size-limit The maximum number of entries that the server should return 731 | ;; :time-limit The maximum length of time in seconds that the server should 732 | ;; spend processing this request 733 | ;; :types-only Return only attribute names instead of names and values 734 | ;; :server-sort Instruct the server to sort the results. The value of this 735 | ;; key is a map like the following: 736 | ;; :is-critical ( true | false ) 737 | ;; :sort-keys [ :cn :ascending 738 | ;; :employeNumber :descending ... ] 739 | ;; At least one sort key must be provided. 740 | ;; :proxied-auth The dn: or u: to be used as the authorization 741 | ;; identity when processing the request. 742 | ;; :controls Adds the provided controls for this request. 743 | ;; :respf Applies this function to all response controls present. 744 | 745 | (defn search-all 746 | "Uses SimplePagedResultsControl to search on the connected ldap server, 747 | and returns the results as a lazy sequence of maps." 748 | ([connection base] 749 | (search-all connection base nil)) 750 | ([connection base options] 751 | (search-all-results connection (search-criteria base options)))) 752 | 753 | (defn search 754 | "Runs a search on the connected ldap server, reads all the results into 755 | memory and returns the results as a sequence of maps." 756 | ([connection base] 757 | (search connection base nil)) 758 | ([connection base options] 759 | (search-results connection (search-criteria base options)))) 760 | 761 | (defn search! 762 | "Runs a search on the connected ldap server and executes the given 763 | function (for side effects) on each result. Does not read all the 764 | results into memory." 765 | ([connection base f] 766 | (search! connection base nil f)) 767 | ([connection base options f] 768 | (search-results! connection (search-criteria base options) f))) 769 | --------------------------------------------------------------------------------