diff --git a/.gitignore b/.gitignore index 7114fec..4a4aaa4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ pom.xml.asc .idea clj-ldap.iml test-resources/access -test-resources/access.lck +test-resources/access.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 1367967..083ff0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.0.17] +This release adds the following: +- A `bind` function which behaves similarly to `bind?` except that it throws `LDAPException` on failure. Thanks to @yogthos and @sumbach. +- A bump of the ldap-sdk to 5.1.1. + ## [0.0.16] This release adds the following: - A lazy-seq returned by `search-all-results` using SimplePagedResults control. Thanks to @jindrichmynarz. diff --git a/README.md b/README.md index 92501e3..80d54aa 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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) ```clojure - :dependencies [[org.clojars.pntblnk/clj-ldap "0.0.16"]] + :dependencies [[org.clojars.pntblnk/clj-ldap "0.0.17"]] ``` # Example @@ -30,7 +30,7 @@ clj-ldap is a thin layer on the [unboundid sdk](http://www.unboundid.com/product ## connect [options] -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). +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). Options is a map with the following entries: :host Either a string in the form "address:port" @@ -83,32 +83,69 @@ a connection is expected. Using a pool in this manner aleviates the caller from release connections. It will still be necessary to get and release a connection if a single connection is needed to process a sequence of operations. See the following bind? example. -## bind? [connection bind-dn password] [connection-pool bind-dn password] +## bind [connection bind-dn password] [connection-pool bind-dn password] Usage: ```clojure - (ldap/bind? pool "cn=dude,ou=people,dc=example,dc=com" "somepass") + ;; Authenticate user with given dn but the pool retains the previous identity on the connection + (try + (ldap/bind pool "cn=dude,ou=people,dc=example,dc=com" "somepass") + (catch LDAPException e + (if (= 49 (.intValue (.getResultCode e))) + (prn "Password invalid") + (throw e)))) (let [conn (ldap/get-connection pool) user-dn "uid=user.1,ou=people,dc=example,dc=com" user-password "password"] (try + ;; Passed a connection, a successful authentication changes the authorization identity of the connection (when (ldap/bind? conn user-dn user-password) (ldap/modify conn user-dn {:replace {:description "On sabatical"}})) (finally (ldap/release-connection pool conn)))) ``` Performs a bind operation using the provided connection, bindDN and -password. Returns true if successful. +password. Returns true if successful and false otherwise. + +When an LDAP connection object is used as the connection argument the +bind? function will attempt to change the identity of that connection +to that of the provided DN. Subsequent operations on that connection +will be done using the bound identity. -If an LDAPConnectionPool object is passed as the connection argument +If an LDAP connection pool object is passed as the connection argument the bind attempt will have no side-effects, leaving the state of the underlying connections unchanged. +Throws a [LDAPException](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPException.html) on unsuccessful authentication or other error. + +## bind? [connection bind-dn password] [connection-pool bind-dn password] + +Usage: +```clojure + ;; Authenticate user with given dn but the pool retains the previous identity on the connection + (ldap/bind? pool "cn=dude,ou=people,dc=example,dc=com" "somepass") + + (let [conn (ldap/get-connection pool) + user-dn "uid=user.1,ou=people,dc=example,dc=com" + user-password "password"] + (try + ;; Passed a connection, a successful authentication changes the authorization identity of the connection + (when (ldap/bind? conn user-dn user-password) + (ldap/modify conn user-dn {:replace {:description "On sabatical"}})) + (finally (ldap/release-connection pool conn)))) +``` +Performs a bind operation using the provided connection, bindDN and +password. Returns true if successful and false otherwise. + When an LDAP connection object is used as the connection argument the bind? function will attempt to change the identity of that connection to that of the provided DN. Subsequent operations on that connection will be done using the bound identity. +If an LDAP connection pool object is passed as the connection argument +the bind attempt will have no side-effects, leaving the state of the +underlying connections unchanged. + ## get [connection dn] [connection dn attributes] If successful, returns a map containing the entry for the given DN. diff --git a/project.clj b/project.clj index 955a1e8..677beb3 100644 --- a/project.clj +++ b/project.clj @@ -1,8 +1,8 @@ -(defproject org.clojars.pntblnk/clj-ldap "0.0.16" +(defproject org.clojars.pntblnk/clj-ldap "0.0.17" :description "Clojure ldap client." :url "https://github.com/pauldorman/clj-ldap" :dependencies [[org.clojure/clojure "1.8.0"] - [com.unboundid/unboundid-ldapsdk "4.0.4"]] + [com.unboundid/unboundid-ldapsdk "5.1.1"]] :profiles {:test {:dependencies [[lein-clojars "0.9.1"]]}} :aot [clj-ldap.client] :license {:name "Eclipse Public License - v 1.0" diff --git a/src/clj_ldap/client.clj b/src/clj_ldap/client.clj index ba93e1e..e60361c 100644 --- a/src/clj_ldap/client.clj +++ b/src/clj_ldap/client.clj @@ -28,7 +28,7 @@ Control StartTLSPostConnectProcessor CompareRequest - CompareResult]) + CompareResult BindResult]) (:import [com.unboundid.ldap.sdk.extensions PasswordModifyExtendedRequest PasswordModifyExtendedResult @@ -93,7 +93,7 @@ adding the DN. We pass along the byte-valued collection to properly return binary data." ([byte-valued] - (entry-as-map byte-valued true)) + (entry-as-map byte-valued true)) ([byte-valued dn?] (fn [entry] (let [attrs (seq (.getAttributes entry))] @@ -109,10 +109,10 @@ (condp instance? control PreReadResponseControl (update-in m [:pre-read] merge ((entry-as-map [] false) - (.getEntry control))) + (.getEntry control))) PostReadResponseControl (update-in m [:post-read] merge ((entry-as-map [] false) - (.getEntry control))) + (.getEntry control))) m)) (defn- add-response-controls @@ -192,6 +192,17 @@ conn) :else (LDAPConnection. opt host ldap-port)))) +(defn- bind-based-on-connection + "Common bind approach for the api: + connection represents pool then authenticate and revert bind association on pool connection, or + connection is plain then authenticate and remain bound. + 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 + connection context but since we make this the default behavior of pool binds it is likely unnecessary." + [connection bind-dn password] + (if (instance? LDAPConnectionPool connection) + (.bindAndRevertAuthentication connection bind-dn password nil) + (.bind connection bind-dn password))) + (defn- bind-request "Returns a BindRequest object" [{:keys [bind-dn password]}] @@ -462,8 +473,8 @@ [(createServerSideSort server-sort)] []) proxied-auth-control (if (not-nil? proxied-auth) - [(ProxiedAuthorizationV2RequestControl. proxied-auth)] - [])] + [(ProxiedAuthorizationV2RequestControl. proxied-auth)] + [])] (merge original {:base base :scope (get-scope scope) :filter filter @@ -524,23 +535,41 @@ [pool connection] (.releaseAndReAuthenticateConnection pool connection)) +(defn bind + "Performs a bind operation using the provided connection or pool, bindDN and + password. If the bind is unsuccessful LDAPException is thrown. Otherwise, a + map is returned with :code, :name, and optional :diagnostic-message keys. + The :diagnostic-message might contain password expiration warnings, for instance. + + When an LDAP connection object is used as the connection argument the + bind function will attempt to change the identity of that connection + to that of the provided DN. Subsequent operations on that connection + will be done using the bound identity. + + If an LDAP connection pool object is passed as the connection argument + the bind attempt will have no side-effects, leaving the state of the + underlying connections unchanged." + [connection bind-dn password] + (let [^BindResult r (bind-based-on-connection connection bind-dn password)] + (merge (ldap-result r) + (when-let [diagnostic-message (.getDiagnosticMessage r)] + {:diagnostic-message diagnostic-message})))) + (defn bind? "Performs a bind operation using the provided connection, bindDN and -password. Returns true if successful. + password. Returns true if successful and false otherwise. -When an LDAP connection object is used as the connection argument the -bind? function will attempt to change the identity of that connection -to that of the provided DN. Subsequent operations on that connection -will be done using the bound identity. + When an LDAP connection object is used as the connection argument the + bind? function will attempt to change the identity of that connection + to that of the provided DN. Subsequent operations on that connection + will be done using the bound identity. -If an LDAP connection pool object is passed as the connection argument -the bind attempt will have no side-effects, leaving the state of the -underlying connections unchanged." + If an LDAP connection pool object is passed as the connection argument + the bind attempt will have no side-effects, leaving the state of the + underlying connections unchanged." [connection bind-dn password] (try - (let [r (if (instance? LDAPConnectionPool connection) - (.bindAndRevertAuthentication connection bind-dn password nil) - (.bind connection bind-dn password))] + (let [r (bind-based-on-connection connection bind-dn password)] (= ResultCode/SUCCESS (.getResultCode r))) (catch Exception _ false))) @@ -583,7 +612,7 @@ underlying connections unchanged." "Adds an entry to the connected ldap server. The entry is assumed to be a map. The options map supports control :proxied-auth." ([connection dn entry] - (add connection dn entry nil)) + (add connection dn entry nil)) ([connection dn entry options] (let [entry-obj (Entry. dn)] (set-entry-map! entry-obj entry) @@ -629,7 +658,7 @@ Where :add adds an attribute value, :delete deletes an attribute value and The entries :pre-read and :post-read specify attributes that have be read and returned either before or after the modifications have taken place." ([connection dn modifications] - (modify connection dn modifications nil)) + (modify connection dn modifications nil)) ([connection dn modifications options] (let [modify-obj (get-modify-request dn modifications)] (when options @@ -662,7 +691,7 @@ returned either before or after the modifications have taken place." RDN value from the target entry. The options map supports pre/post-read and proxied-auth controls." ([connection dn new-rdn delete-old-rdn] - (modify-rdn connection dn new-rdn delete-old-rdn nil)) + (modify-rdn connection dn new-rdn delete-old-rdn nil)) ([connection dn new-rdn delete-old-rdn options] (let [request (ModifyDNRequest. dn new-rdn delete-old-rdn)] (when options diff --git a/test-resources/server.keystore b/test-resources/server.keystore index b974252..9244a33 100644 Binary files a/test-resources/server.keystore and b/test-resources/server.keystore differ diff --git a/test/clj_ldap/test/client.clj b/test/clj_ldap/test/client.clj index 20e8417..95ed0c2 100644 --- a/test/clj_ldap/test/client.clj +++ b/test/clj_ldap/test/client.clj @@ -2,7 +2,8 @@ "Automated tests for clj-ldap" (:require [clj-ldap.client :as ldap] [clj-ldap.test.server :as server]) - (:use clojure.test)) + (:use clojure.test) + (:import (com.unboundid.ldap.sdk LDAPException))) ;; Tests are run over a variety of connection types (LDAP and LDAPS for now) @@ -130,14 +131,24 @@ (use-fixtures :once test-server) (deftest test-get - (is (= (ldap/get *conn* (:dn person-a*)) - (assoc (:object person-a*) :dn (:dn person-a*)))) - (is (= (ldap/get *conn* (:dn person-b*)) - (assoc (:object person-b*) :dn (:dn person-b*)))) - (is (= (ldap/get *conn* (:dn person-a*) [:cn :sn]) - {:dn (:dn person-a*) + (is (= (assoc (:object person-a*) :dn (:dn person-a*)) + (ldap/get *conn* (:dn person-a*)))) + (is (= (assoc (:object person-b*) :dn (:dn person-b*)) + (ldap/get *conn* (:dn person-b*)))) + (is (= {:dn (:dn person-a*) :cn (-> person-a* :object :cn) - :sn (-> person-a* :object :sn)}))) + :sn (-> person-a* :object :sn)} + (ldap/get *conn* (:dn person-a*) [:cn :sn])))) + +(deftest some-bind + (is (= {:code 0, :name "success"} + (ldap/bind *conn* (:dn person-a*) "passa"))) + (is (thrown? LDAPException (ldap/bind *conn* (:dn person-a*) "notthepass"))) + (is (thrown? LDAPException (ldap/bind *conn* "cn=does,ou=not,cn=exist" "password")))) + +(deftest bad-bind? + (is (= false (ldap/bind? *conn* (:dn person-a*) "not-the-password"))) + (is (= false (ldap/bind? *conn* "cn=does,ou=not,cn=exist" "password")))) (deftest test-bind (if (> (-> *conn* @@ -148,164 +159,158 @@ _ (ldap/bind? *c* (:dn person-a*) "passa") a (ldap/who-am-i *c*) _ (ldap/release-connection *conn* *c*)] - (is (= [before a] - ["" (:dn person-a*)])))))) + (is (= ["" (:dn person-a*)] [before a])))))) (deftest test-add-delete - (is (= (ldap/add *conn* (:dn person-c*) (:object person-c*)) - success*)) - (is (= (ldap/get *conn* (:dn person-c*)) - (assoc (:object person-c*) :dn (:dn person-c*)))) - (is (= (ldap/delete *conn* (:dn person-c*)) - success*)) + (is (= success* (ldap/add *conn* (:dn person-c*) (:object person-c*)))) + (is (= (assoc (:object person-c*) :dn (:dn person-c*)) + (ldap/get *conn* (:dn person-c*)))) + (is (= success* (ldap/delete *conn* (:dn person-c*)))) (is (nil? (ldap/get *conn* (:dn person-c*)))) - (is (= (ldap/add *conn* (str "changeNumber=1234," base*) + (is (= success* + (ldap/add *conn* (str "changeNumber=1234," base*) {:objectClass ["changeLogEntry"] :changeNumber 1234 :targetDN base* - :changeType "modify"}) - success*)) - (is (= (:changeNumber (ldap/get *conn* (str "changeNumber=1234," base*))) - "1234")) - (is (= (ldap/delete *conn* (str "changeNumber=1234," base*) - {:pre-read [:objectClass]}) - {:code 0, :name "success", - :pre-read {:objectClass #{"top" "changeLogEntry"}}}))) + :changeType "modify"}))) + (is (= "1234" (:changeNumber (ldap/get *conn* (str "changeNumber=1234," base*))))) + (is (= {:code 0, :name "success", + :pre-read {:objectClass #{"top" "changeLogEntry"}}} + (ldap/delete *conn* (str "changeNumber=1234," base*) + {:pre-read [:objectClass]})))) (deftest test-delete-subtree - (is (= (ldap/add *conn* (:dn person-c*) (:object person-c*)) - success*)) - (is (= (ldap/delete *conn* base* {:delete-subtree true}) - success*)) + (is (= success* (ldap/add *conn* (:dn person-c*) (:object person-c*)))) + (is (= success* (ldap/delete *conn* base* {:delete-subtree true}))) (is (nil? (ldap/get *conn* base*)))) (deftest test-modify-add - (is (= (ldap/modify *conn* (:dn person-a*) + (is (= {:code 0, :name "success", + :pre-read {:objectClass #{"top" "person"}, :cn "testa"}, + :post-read {:l "Hollywood", :cn "testa"}} + (ldap/modify *conn* (:dn person-a*) {:add {:objectClass "organizationalPerson" :l "Hollywood"} :pre-read #{:objectClass :l :cn} - :post-read #{:l :cn}}) - {:code 0, :name "success", - :pre-read {:objectClass #{"top" "person"}, :cn "testa"}, - :post-read {:l "Hollywood", :cn "testa"}})) - (is (= (ldap/modify - *conn* (:dn person-b*) - {:add {:telephoneNumber ["0000000005" "0000000006"]}}) - success*)) + :post-read #{:l :cn}}))) + (is (= success* + (ldap/modify *conn* (:dn person-b*) + {:add {:telephoneNumber ["0000000005" "0000000006"]}}))) (let [new-a (ldap/get *conn* (:dn person-a*)) new-b (ldap/get *conn* (:dn person-b*)) obj-a (:object person-a*) obj-b (:object person-b*)] - (is (= (:objectClass new-a) - (conj (:objectClass obj-a) "organizationalPerson"))) - (is (= (:l new-a) "Hollywood")) - (is (= (set (:telephoneNumber new-b)) - (set (concat (:telephoneNumber obj-b) - ["0000000005" "0000000006"])))))) + (is (= (conj (:objectClass obj-a) "organizationalPerson") + (:objectClass new-a))) + (is (= "Hollywood" (:l new-a))) + (is (= (set (concat (:telephoneNumber obj-b) + ["0000000005" "0000000006"])) + (set (:telephoneNumber new-b)))))) (deftest test-modify-delete (let [b-phonenums (-> person-b* :object :telephoneNumber)] - (is (= (ldap/modify *conn* (:dn person-a*) - {:delete {:description :all}}) - success*)) - (is (= (ldap/modify *conn* (:dn person-b*) - {:delete {:telephoneNumber (first b-phonenums)}}) - success*)) - (is (= (ldap/get *conn* (:dn person-a*)) - (-> (:object person-a*) + (is (= success* + (ldap/modify *conn* (:dn person-a*) + {:delete {:description :all}}))) + (is (= success* + (ldap/modify *conn* (:dn person-b*) + {:delete {:telephoneNumber (first b-phonenums)}}))) + (is (= (-> (:object person-a*) (dissoc :description) - (assoc :dn (:dn person-a*))))) - (is (= (ldap/get *conn* (:dn person-b*)) - (-> (:object person-b*) + (assoc :dn (:dn person-a*))) + (ldap/get *conn* (:dn person-a*)))) + (is (= (-> (:object person-b*) (assoc :telephoneNumber (second b-phonenums)) - (assoc :dn (:dn person-b*))))))) + (assoc :dn (:dn person-b*))) + (ldap/get *conn* (:dn person-b*)))))) (deftest test-modify-replace (let [new-phonenums (-> person-b* :object :telephoneNumber) certificate-data (read-bytes-from-file "test-resources/cert.binary")] - (is (= (ldap/modify *conn* (:dn person-a*) - {:replace {:telephoneNumber new-phonenums}}) - success*)) - (is (= (ldap/get *conn* (:dn person-a*)) - (-> (:object person-a*) + (is (= success* + (ldap/modify *conn* (:dn person-a*) + {:replace {:telephoneNumber new-phonenums}}))) + (is (= (-> (:object person-a*) (assoc :telephoneNumber new-phonenums) - (assoc :dn (:dn person-a*))))) - (is (= (ldap/modify *conn* (:dn person-a*) + (assoc :dn (:dn person-a*))) + (ldap/get *conn* (:dn person-a*)))) + + (is (= success* + (ldap/modify *conn* (:dn person-a*) {:add {:objectclass ["inetOrgPerson" "organizationalPerson"] :userCertificate certificate-data}} - {:proxied-auth (str "dn:" (:dn person-a*))}) - success*)) - (is (= (seq (:userCertificate + {:proxied-auth (str "dn:" (:dn person-a*))}))) + (is (= (seq certificate-data) + (seq (:userCertificate (first (ldap/search *conn* (:dn person-a*) {:scope :base :filter "(objectclass=inetorgperson)" :attributes [:userCertificate] - :byte-valued [:userCertificate]})))) - (seq certificate-data))) - (is (= (seq (:userCertificate + :byte-valued [:userCertificate]})))))) + (is (= (seq certificate-data) + (seq (:userCertificate (first (ldap/search *conn* (:dn person-a*) {:scope :base - :byte-valued [:userCertificate]})))) - (seq certificate-data))) - (is (= (seq (:userCertificate (ldap/get *conn* (:dn person-a*) + :byte-valued [:userCertificate]})))))) + (is (= (seq certificate-data) + (seq (:userCertificate (ldap/get *conn* (:dn person-a*) [:userCertificate] - [:userCertificate]))) - (seq certificate-data))))) + [:userCertificate]))))))) (deftest test-modify-all (let [b (:object person-b*) b-phonenums (:telephoneNumber b)] - (is (= (ldap/modify *conn* (:dn person-b*) + (is (= success* + (ldap/modify *conn* (:dn person-b*) {:add {:telephoneNumber "0000000005"} :delete {:telephoneNumber (second b-phonenums)} - :replace {:description "desc x"}}) - success*)) + :replace {:description "desc x"}}))) (let [new-b (ldap/get *conn* (:dn person-b*))] - (is (= (set (:telephoneNumber new-b)) - (set [(first b-phonenums) "0000000005"]))) - (is (= (:description new-b) "desc x"))))) + (is (= (set [(first b-phonenums) "0000000005"]) + (set (:telephoneNumber new-b)))) + (is (= "desc x" (:description new-b)))))) (deftest test-search - (is (= (set (map :cn - (ldap/search *conn* base* {:attributes [:cn]}))) - (set [nil "testa" "testb" "Saul Hazledine"]))) - (is (= (set (map :cn + (is (= (set [nil "testa" "testb" "Saul Hazledine"]) + (set (map :cn + (ldap/search *conn* base* {:attributes [:cn]}))))) + (is (= (set ["testa" "testb"]) + (set (map :cn (ldap/search *conn* base* {:attributes [:cn] :filter "cn=test*" - :proxied-auth (str "dn:" (:dn person-a*))}))) - (set ["testa" "testb"]))) - (is (= (map :cn + :proxied-auth (str "dn:" (:dn person-a*))}))))) + (is (= '("Saul Hazledine" "testa" "testb") + (map :cn (ldap/search *conn* base* {:filter "cn=*" :server-sort {:is-critical true - :sort-keys [:cn :ascending]}})) - '("Saul Hazledine" "testa" "testb"))) - (is (= (count (map :cn - (ldap/search *conn* base* - {:attributes [:cn] :filter "cn=*" - :size-limit 2}))) - 2)) - (is (= (:description (map :cn - (ldap/search *conn* base* - {:attributes [:cn] - :filter "cn=István Orosz" - :types-only true}))) - nil)) - (is (= (set (map :description + :sort-keys [:cn :ascending]}})))) + + (is (= 2 (count (map :cn + (ldap/search *conn* base* + {:attributes [:cn] :filter "cn=*" + :size-limit 2}))))) + (is (= nil (:description (map :cn + (ldap/search *conn* base* + {:attributes [:cn] + :filter "cn=István Orosz" + :types-only true}))))) + (is (= (set ["István Orosz"]) + (set (map :description (ldap/search *conn* base* - {:filter "cn=testb" :types-only false}))) - (set ["István Orosz"]))) + {:filter "cn=testb" :types-only false}))))) (binding [*side-effects* #{}] (ldap/search! *conn* base* {:attributes [:cn :sn] :filter "cn=test*"} (fn [x] (set! *side-effects* (conj *side-effects* (dissoc x :dn))))) - (is (= *side-effects* - (set [{:cn "testa" :sn "a"} - {:cn "testb" :sn "b"}]))))) + (is (= (set [{:cn "testa" :sn "a"} + {:cn "testb" :sn "b"}]) + *side-effects*)))) + (deftest test-search-all (let [options {:attributes [:cn] :page-size 2} @@ -319,10 +324,8 @@ (->cn-set eager-results))))) (deftest test-compare? - (is (= (ldap/compare? *conn* (:dn person-b*) - :description "István Orosz") - true)) - (is (= (ldap/compare? *conn* (:dn person-a*) - :description "István Orosz" - {:proxied-auth (str "dn:" (:dn person-b*))}) - false))) + (is (= true (ldap/compare? *conn* (:dn person-b*) + :description "István Orosz"))) + (is (= false (ldap/compare? *conn* (:dn person-a*) + :description "István Orosz" + {:proxied-auth (str "dn:" (:dn person-b*))})))) diff --git a/test/clj_ldap/test/server.clj b/test/clj_ldap/test/server.clj index df7aef1..227bf76 100644 --- a/test/clj_ldap/test/server.clj +++ b/test/clj_ldap/test/server.clj @@ -27,6 +27,9 @@ (.setFormatter fileHandler (MinimalLogFormatter. nil false false true)) fileHandler)) +;; Server's certificate, with alias "server-cert" generated using keytool like so: +;; keytool -genkey -keyalg RSA -alias server-cert -keystore selfsigned.jks -validity 3650 -keysize 2048 + (defn- start-ldap-server "Setup a server listening on available LDAP and LDAPS ports chosen at random" []