Permalink
Browse files

Initial put

  • Loading branch information...
0 parents commit 7607791f86f773a77bb2ae123ff80a0cd75dc48a @alienscience committed Feb 6, 2011
Showing with 487 additions and 0 deletions.
  1. +75 −0 README.md
  2. +14 −0 project.clj
  3. +152 −0 src/clj_ldap/client.clj
  4. +153 −0 test/clj_ldap/test/client.clj
  5. +93 −0 test/clj_ldap/test/server.clj
@@ -0,0 +1,75 @@
+
+# Introduction
+
+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.
+
+# Example
+
+ (ns example
+ (:require [clj-ldap.client :as ldap]))
+
+ (let [connection (ldap/connect {:address "ldap.example.com"})]
+ (ldap/get connection "cn=dude,ou=people,dc=example,dc=com"))
+
+ ;; Returns a map such as
+ {:gidNumber "2000"
+ :loginShell "/bin/bash"
+ :objectClass #{"inetOrgPerson" "posixAccount" "shadowAccount"}
+ :mail "dude@example.com"
+ :sn "Dudeness"
+ :cn "dude"
+ :uid "dude"
+ :homeDirectory "/home/dude"}
+
+# API
+
+## connect [options]
+
+Connects to an ldap server and returns an [LDAPConnectionPool](http://www.unboundid.com/products/ldap-sdk/docs/javadoc/com/unboundid/ldap/sdk/LDAPConnectionPool.html).
+Options is a map with the following entries:
+ :address Address of server, defaults to localhost
+ :port Port to connect to, defaults to 389
+ :bind-dn The DN to bind as, can be a map or string, optional
+ :password The password to bind with, optional
+ :num-connections The number of connections in the pool, defaults to 1
+
+## get [connection dn]
+
+If successful, returns a map containing the entry for the given DN.
+Returns nil if the entry doesn't exist or cannot be read.
+
+ (ldap/connect conn {:address "ldap.example.com" :num-connections 10})
+
+## add [connection dn entry]
+
+Adds an entry to the connected ldap server. The entry is map of keywords to values which can be strings, sets or vectors.
+
+ (ldap/add conn "cn=dude,ou=people,dc=example,dc=com"
+ {:objectClass #{"top" "person"}
+ :cn "dude"
+ :sn "a"
+ :description "His dudeness"
+ :telephoneNumber ["1919191910" "4323324566"]})
+
+## modify [connection dn modifications]
+
+Modifies an entry in the connected ldap server. The modifications are
+a map in the form:
+ {:add
+ {:attribute-a some-value
+ :attribute-b [value1 value2]}
+ :delete
+ {:attribute-c :all
+ :attribute-d some-value
+ :attribute-e [value1 value2]}
+ :replace
+ {:attibute-d value
+ :attribute-e [value1 value2]}
+ :increment [:attribute-f attribute-g]}
+
+## delete [connection dn]
+
+Deletes the entry with the given DN on the connected ldap server.
+
+ (ldap/delete conn "cn=dude,ou=people,dc=example,dc=com")
+
@@ -0,0 +1,14 @@
+(defproject clj-ldap "0.0.0"
+ :description "Clojure ldap client"
+ :dependencies [[org.clojure/clojure "1.2.0"]
+ [org.clojure/clojure-contrib "1.2.0"]
+ [com.unboundid/unboundid-ldapsdk "2.0.0"]]
+ :dev-dependencies [[swank-clojure "1.2.1"]
+ [org.apache.directory.server/apacheds-all "1.5.5"]
+ [org.slf4j/slf4j-simple "1.5.6"]
+ [clj-file-utils "0.2.1"]]
+ :license {:name "Eclipse Public License - v 1.0"
+ :url "http://www.eclipse.org/legal/epl-v10.html"
+ :distribution :repo
+ :comments "same as Clojure"})
+
@@ -0,0 +1,152 @@
+
+(ns clj-ldap.client
+ "LDAP client"
+ (:refer-clojure :exclude [get])
+ (:require [clojure.string :as string])
+ (:import [com.unboundid.ldap.sdk
+ LDAPResult
+ LDAPConnection
+ ResultCode
+ LDAPConnectionPool
+ LDAPException
+ Attribute
+ Entry
+ ModificationType
+ ModifyRequest
+ Modification
+ DeleteRequest]))
+
+;;======== Helper functions ====================================================
+
+(defn- ldap-result
+ "Converts an LDAPResult object into a clojure datastructure"
+ [obj]
+ (let [rc (.getResultCode obj)]
+ [(.intValue rc) (.getName rc)]))
+
+(defn- extract-attribute
+ "Extracts [:name value] from the given attribute object. Converts
+ the objectClass attribute to a set."
+ [attr]
+ (let [k (keyword (.getName attr))]
+ (cond
+ (= :objectClass k) [k (set (vec (.getValues attr)))]
+ (> (.size attr) 1) [k (vec (.getValues attr))]
+ :else [k (.getValue attr)])))
+
+
+(defn- set-entry-kv!
+ "Sets the given key/value pair in the given entry object"
+ [entry-obj k v]
+ (let [name-str (name k)]
+ (.addAttribute entry-obj
+ (if (coll? v)
+ (Attribute. name-str (into-array v))
+ (Attribute. name-str (str v))))))
+
+(defn- set-entry-map!
+ "Sets the attributes in the given entry object using the given map"
+ [entry-obj m]
+ (doseq [[k v] m]
+ (set-entry-kv! entry-obj k v)))
+
+(defn- create-modification
+ "Creates a modification object"
+ [modify-op attribute values]
+ (cond
+ (coll? values) (Modification. modify-op attribute (into-array values))
+ (= :all values) (Modification. modify-op attribute)
+ :else (Modification. modify-op attribute (str values))))
+
+(defn- modify-ops
+ "Returns a sequence of Modification objects to do the given operation
+ using the contents of the given map."
+ [modify-op modify-map]
+ (for [[k v] modify-map]
+ (create-modification modify-op (name k) v)))
+
+(defn- modify-incs
+ "Returns a sequence of Modification objects that increment the given
+ attribute(s)"
+ [attributes]
+ (if attributes
+ (if (coll? attributes)
+ (map #(Modification. ModificationType/INCREMENT %) attributes)
+ [(Modification. ModificationType/INCREMENT attributes)])))
+
+(defn- get-modify-request
+ "Sets up a ModifyRequest object using the contents of the given map"
+ [dn modifications]
+ (let [adds (modify-ops ModificationType/ADD (modifications :add))
+ deletes (modify-ops ModificationType/DELETE (modifications :delete))
+ replacements (modify-ops ModificationType/REPLACE
+ (modifications :replace))
+ incs (modify-incs (modifications :increment))]
+ (ModifyRequest. dn (into-array (concat adds deletes replacements incs)))))
+
+;;=========== API ==============================================================
+
+(defn connect
+ "Connects to an ldap server and returns an LDAPConnectionPool.
+ Options is a map with the following entries:
+ :address Address of server, defaults to localhost
+ :port Port to connect to, defaults to 389
+ :bind-dn The DN to bind as, can be a map or string, optional
+ :password The password to bind with, optional
+ :num-connections The number of connections in the pool, defaults to 1
+ "
+ [{:keys [address port bind-dn password num-connections] :as options}]
+ (let [connection (LDAPConnection. (or address "localhost") (or port 389))
+ bind-result (.bind connection bind-dn password)]
+ (if (= ResultCode/SUCCESS (.getResultCode bind-result))
+ (LDAPConnectionPool. connection (or num-connections 1))
+ (throw (LDAPException. bind-result)))))
+
+(defn get
+ "If successful, returns a map containing the entry for the given DN.
+ Returns nil if the entry doesn't exist or cannot be read."
+ [connection dn]
+ (if-let [result (.getEntry connection dn)]
+ (let [attrs (seq (.getAttributes result))]
+ (apply hash-map
+ (mapcat extract-attribute attrs)))))
+
+(defn add
+ "Adds an entry to the connected ldap server. The entry is assumed to be
+ a map."
+ [connection dn entry]
+ (let [entry-obj (Entry. dn)]
+ (set-entry-map! entry-obj entry)
+ (ldap-result
+ (.add connection entry-obj))))
+
+(defn modify
+ "Modifies an entry in the connected ldap server. The modifications are
+ a map in the form:
+ {:add
+ {:attribute-a some-value
+ :attribute-b [value1 value2]}
+ :delete
+ {:attribute-c :all
+ :attribute-d some-value
+ :attribute-e [value1 value2]}
+ :replace
+ {:attibute-d value
+ :attribute-e [value1 value2]}
+ :increment [:attribute-f attribute-g]}
+"
+ [connection dn modifications]
+ (let [modify-obj (get-modify-request dn modifications)]
+ (ldap-result
+ (.modify connection modify-obj))))
+
+
+(defn delete
+ "Deletes the given entry in the connected ldap server"
+ [connection dn]
+ (let [delete-obj (DeleteRequest. dn)]
+ (ldap-result
+ (.delete connection delete-obj))))
+
+
+
@@ -0,0 +1,153 @@
+
+(ns clj-ldap.test.client
+ "Automated tests for clj-ldap"
+ (:require [clj-ldap.client :as ldap])
+ (:require [clj-ldap.test.server :as server])
+ (:use clojure.test)
+ (:import [com.unboundid.ldap.sdk LDAPException]))
+
+
+;; Tests are run over a variety of connection types
+(def port* 8000)
+(def *connections* nil)
+(def *conn* nil)
+
+;; Tests concentrate on a single object class
+(def dn* "cn=%s,ou=people,dc=alienscience,dc=org,dc=uk")
+(def object-class* #{"top" "person"})
+
+;; Result of a successful write
+(def success* [0 "success"])
+
+;; People to test with
+(def person-a*
+ {:dn (format dn* "testa")
+ :object {:objectClass object-class*
+ :cn "testa"
+ :sn "a"
+ :description "description a"
+ :telephoneNumber "000000001"
+ :userPassword "passa"}})
+
+(def person-b*
+ {:dn (format dn* "testb")
+ :object {:objectClass object-class*
+ :cn "testb"
+ :sn "b"
+ :description "description b"
+ :telephoneNumber ["000000002" "00000003"]
+ :userPassword "passb"}})
+
+(def person-c*
+ {:dn (format dn* "testc")
+ :object {:objectClass object-class*
+ :cn "testc"
+ :sn "c"
+ :description "description c"
+ :telephoneNumber "000000004"
+ :userPassword "passc"}})
+
+(defn- connect-to-server
+ "Opens a sequence of connection pools on the localhost server with the
+ given port"
+ [port]
+ [(ldap/connect {:port port})
+ (ldap/connect {:address "localhost"
+ :port port
+ :num-connections 4})])
+
+(defn- test-server
+ "Setup server"
+ [f]
+ (server/start! port*)
+ (binding [*connections* (connect-to-server port*)]
+ (f))
+ (server/stop!))
+
+(defn- test-data
+ "Provide test data"
+ [f]
+ (doseq [connection *connections*]
+ (binding [*conn* connection]
+ (try
+ (ldap/add *conn* (:dn person-a*) (:object person-a*))
+ (ldap/add *conn* (:dn person-b*) (:object person-b*))
+ (catch Exception e))
+ (f)
+ (try
+ (ldap/delete *conn* (:dn person-a*))
+ (ldap/delete *conn* (:dn person-b*))
+ (catch Exception e)))))
+
+(use-fixtures :each test-data)
+(use-fixtures :once test-server)
+
+(deftest test-get
+ (is (= (ldap/get *conn* (:dn person-a*))
+ (:object person-a*)))
+ (is (= (ldap/get *conn* (:dn person-b*))
+ (:object person-b*))))
+
+(deftest test-add-delete
+ (is (= (ldap/add *conn* (:dn person-c*) (:object person-c*))
+ success*))
+ (is (= (ldap/get *conn* (:dn person-c*))
+ (:object person-c*)))
+ (is (= (ldap/delete *conn* (:dn person-c*))
+ success*))
+ (is (nil? (ldap/get *conn* (:dn person-c*)))))
+
+(deftest test-modify-add
+ (is (= (ldap/modify *conn* (:dn person-a*)
+ {:add {:objectClass "residentialperson"
+ :l "Hollywood"}})
+ success*))
+ (is (= (ldap/modify
+ *conn* (:dn person-b*)
+ {:add {:telephoneNumber ["0000000005" "0000000006"]}})
+ success*))
+ (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) "residentialPerson")))
+ (is (= (:l new-a) "Hollywood"))
+ (is (= (set (:telephoneNumber new-b))
+ (set (concat (:telephoneNumber obj-b)
+ ["0000000005" "0000000006"]))))))
+
+(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*))
+ (dissoc (:object person-a*) :description)))
+ (is (= (ldap/get *conn* (:dn person-b*))
+ (assoc (:object person-b*) :telephoneNumber (second b-phonenums))))))
+
+(deftest test-modify-replace
+ (let [new-phonenums (-> person-b* :object :telephoneNumber)]
+ (is (= (ldap/modify *conn* (:dn person-a*)
+ {:replace {:telephoneNumber new-phonenums}})
+ success*))
+ (is (= (ldap/get *conn* (:dn person-a*))
+ (assoc (:object person-a*) :telephoneNumber new-phonenums)))))
+
+(deftest test-modify-all
+ (let [b (:object person-b*)
+ b-phonenums (:telephoneNumber b)]
+ (is (= (ldap/modify *conn* (:dn person-b*)
+ {:add {:telephoneNumber "0000000005"}
+ :delete {:telephoneNumber (second b-phonenums)}
+ :replace {:description "desc x"}})
+ success*))
+ (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")))))
+
Oops, something went wrong.

0 comments on commit 7607791

Please sign in to comment.