fakeldap 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +3 -0
  3. data/lib/fakeldap.rb +46 -0
  4. data/lib/fakeldap/version.rb +4 -0
  5. data/vendor/ruby-ldapserver/COPYING +27 -0
  6. data/vendor/ruby-ldapserver/ChangeLog +83 -0
  7. data/vendor/ruby-ldapserver/Manifest.txt +32 -0
  8. data/vendor/ruby-ldapserver/README +222 -0
  9. data/vendor/ruby-ldapserver/Rakefile +22 -0
  10. data/vendor/ruby-ldapserver/examples/README +89 -0
  11. data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
  12. data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
  13. data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
  14. data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
  15. data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
  16. data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
  17. data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
  18. data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
  19. data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
  20. data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
  21. data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
  22. data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
  23. data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
  24. data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
  25. data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
  26. data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
  27. data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
  28. data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
  29. data/vendor/ruby-ldapserver/test/core.schema +582 -0
  30. data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
  31. data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
  32. data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
  33. data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
  34. data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
  35. data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
  36. data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
  37. metadata +130 -0
@@ -0,0 +1,89 @@
1
+ Using the example programs
2
+ ==========================
3
+
4
+ These servers all listen on port 1389 by default, so that they don't have to
5
+ be run as root.
6
+
7
+ Example 1: trivial server using RAM hash
8
+ ----------------------------------------
9
+
10
+ $ ruby rbslapd1.rb
11
+
12
+ In another window:
13
+
14
+ $ ldapadd -H ldap://127.0.0.1:1389/
15
+ dn: dc=example,dc=com
16
+ cn: Top object
17
+
18
+ dn: cn=Fred Flintstone,dc=example,dc=com
19
+ cn: Fred Flintstone
20
+ sn: Flintstone
21
+ mail: fred@bedrock.org
22
+ mail: fred.flintstone@bedrock.org
23
+
24
+ dn: cn=Wilma Flintstone,dc=example,dc=com
25
+ cn: Wilma Flintstone
26
+ mail: wilma@bedrock.org
27
+ ^D
28
+
29
+ Try these queries:
30
+
31
+ $ ldapsearch -H ldap://127.0.0.1:1389/ -b "" "(objectclass=*)"
32
+ $ ldapsearch -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" -s base "(objectclass=*)"
33
+ $ ldapsearch -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(mail=fred*)"
34
+
35
+ If you terminate the server with Ctrl-C, its contents should be written
36
+ to disk as a YAML file.
37
+
38
+ A fairly complete set of the filter language is implemented. However, this
39
+ simple server works by simply scanning the entire database and applying the
40
+ filter to each entry, so it won't scale to large applications. No validation
41
+ of DN or attributes against any sort of schema is done.
42
+
43
+ Example 1a: with SSL
44
+ --------------------
45
+
46
+ In rbslapd1.rb, uncomment
47
+
48
+ :ssl_key_file => "key.pem",
49
+ :ssl_cert_file => "cert.pem",
50
+ :ssl_on_connect => true,
51
+
52
+ and run mkcert.rb. Since this is a self-signed certificate, you'll have to
53
+ turn off certificate verification in the client too. For example:
54
+
55
+ $ env LDAPTLS_REQCERT="allow" ldapsearch -H ldaps://127.0.0.1:1389/
56
+
57
+ Making your own CA and installing its certificate in the client, or
58
+ generating a Certificate Signing Request and sending it to a known CA, is
59
+ beyond the scope of this documentation.
60
+
61
+ Example 2: simple LDAP to SQL mapping
62
+ -------------------------------------
63
+
64
+ You will need to set up a MySQL database with a table conforming to the
65
+ schema given within the code. Once done, LDAP gives a read-only view of the
66
+ database with only the filter "(uid=<foo>)" supported.
67
+
68
+ Example 3: preforking server and schema
69
+ ---------------------------------------
70
+
71
+ This functions in the same way as rbslapd1.rb. However, since each query is
72
+ answered in a separate process, the YAML file on disk is used as the master
73
+ repository. Update operations re-write this file each time.
74
+
75
+ Also, the schema is read from file 'core.schema'. Attempting to insert the
76
+ above entries will fail, due to schema violations. Insert a valid entry,
77
+ e.g.
78
+
79
+ dn: cn=Fred Flintstone,dc=example,dc=com
80
+ objectClass: organizationalPerson
81
+ cn: Fred Flintstone
82
+ sn: Flintstone
83
+ telephoneNumber: +1 555 1234
84
+ telephoneNumber: +1 555 5432
85
+
86
+ Schema validation takes place for the attribute values and that attributes
87
+ are allowed/required by the objectclass(es); however, the DN itself is not
88
+ validated, nor any checks made that the RDN is present as an attribute
89
+ (since this is one of the more stupid parts of the LDAP/X500 data model)
@@ -0,0 +1,31 @@
1
+ require 'openssl'
2
+
3
+ # Taken directly from echo_svr.rb in the Ruby openssl examples
4
+
5
+ key = OpenSSL::PKey::RSA.new(1024){ print "."; $stdout.flush }
6
+ puts
7
+ cert = OpenSSL::X509::Certificate.new
8
+ cert.version = 2
9
+ cert.serial = 0
10
+ name = OpenSSL::X509::Name.new([["C","JP"],["O","TEST"],["CN","localhost"]])
11
+ cert.subject = name
12
+ cert.issuer = name
13
+ cert.not_before = Time.now
14
+ cert.not_after = Time.now + 3600
15
+ cert.public_key = key.public_key
16
+ ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
17
+ cert.extensions = [
18
+ ef.create_extension("basicConstraints","CA:FALSE"),
19
+ ef.create_extension("subjectKeyIdentifier","hash"),
20
+ ef.create_extension("extendedKeyUsage","serverAuth"),
21
+ ef.create_extension("keyUsage",
22
+ "keyEncipherment,dataEncipherment,digitalSignature")
23
+ ]
24
+ ef.issuer_certificate = cert
25
+ cert.add_extension ef.create_extension("authorityKeyIdentifier",
26
+ "keyid:always,issuer:always")
27
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
28
+
29
+ # Write to disk
30
+ File.open("key.pem","w",0600) { |f| f << key.to_pem }
31
+ File.open("cert.pem","w",0644) { |f| f << cert.to_pem }
@@ -0,0 +1,111 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # This is a trivial LDAP server which just stores directory entries in RAM.
4
+ # It does no validation or authentication. This is intended just to
5
+ # demonstrate the API, it's not for real-world use!!
6
+
7
+ $:.unshift('../lib')
8
+ $debug = true
9
+
10
+ require 'ldap/server'
11
+
12
+ # We subclass the Operation class, overriding the methods to do what we need
13
+
14
+ class HashOperation < LDAP::Server::Operation
15
+ def initialize(connection, messageID, hash)
16
+ super(connection, messageID)
17
+ @hash = hash # an object reference to our directory data
18
+ end
19
+
20
+ def search(basedn, scope, deref, filter)
21
+ basedn.downcase!
22
+
23
+ case scope
24
+ when LDAP::Server::BaseObject
25
+ # client asked for single object by DN
26
+ obj = @hash[basedn]
27
+ raise LDAP::ResultError::NoSuchObject unless obj
28
+ send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj)
29
+
30
+ when LDAP::Server::WholeSubtree
31
+ @hash.each do |dn, av|
32
+ next unless dn.index(basedn, -basedn.length) # under basedn?
33
+ next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
34
+ send_SearchResultEntry(dn, av)
35
+ end
36
+
37
+ else
38
+ raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
39
+
40
+ end
41
+ end
42
+
43
+ def add(dn, av)
44
+ dn.downcase!
45
+ raise LDAP::ResultError::EntryAlreadyExists if @hash[dn]
46
+ @hash[dn] = av
47
+ end
48
+
49
+ def del(dn)
50
+ dn.downcase!
51
+ raise LDAP::ResultError::NoSuchObject unless @hash.has_key?(dn)
52
+ @hash.delete(dn)
53
+ end
54
+
55
+ def modify(dn, ops)
56
+ entry = @hash[dn]
57
+ raise LDAP::ResultError::NoSuchObject unless entry
58
+ ops.each do |attr, vals|
59
+ op = vals.shift
60
+ case op
61
+ when :add
62
+ entry[attr] ||= []
63
+ entry[attr] += vals
64
+ entry[attr].uniq!
65
+ when :delete
66
+ if vals == []
67
+ entry.delete(attr)
68
+ else
69
+ vals.each { |v| entry[attr].delete(v) }
70
+ end
71
+ when :replace
72
+ entry[attr] = vals
73
+ end
74
+ entry.delete(attr) if entry[attr] == []
75
+ end
76
+ end
77
+ end
78
+
79
+ # This is the shared object which carries our actual directory entries.
80
+ # It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
81
+
82
+ directory = {}
83
+
84
+ # Let's put some backing store on it
85
+
86
+ require 'yaml'
87
+ begin
88
+ File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
89
+ rescue Errno::ENOENT
90
+ end
91
+
92
+ at_exit do
93
+ File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
94
+ File.rename("ldapdb.new","ldapdb.yaml")
95
+ end
96
+
97
+ # Listen for incoming LDAP connections. For each one, create a Connection
98
+ # object, which will invoke a HashOperation object for each request.
99
+
100
+ s = LDAP::Server.new(
101
+ :port => 1389,
102
+ :nodelay => true,
103
+ :listen => 10,
104
+ # :ssl_key_file => "key.pem",
105
+ # :ssl_cert_file => "cert.pem",
106
+ # :ssl_on_connect => true,
107
+ :operation_class => HashOperation,
108
+ :operation_args => [directory]
109
+ )
110
+ s.run_tcpserver
111
+ s.join
@@ -0,0 +1,161 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ $:.unshift('../lib')
4
+ require 'ldap/server'
5
+ require 'mysql' # <http://www.tmtm.org/en/ruby/mysql/>
6
+ require 'thread'
7
+ require 'resolv-replace' # ruby threading DNS client
8
+
9
+ # An example of an LDAP to SQL gateway. We have a MySQL table which
10
+ # contains (login_id,login,passwd) combinations, e.g.
11
+ #
12
+ # +----------+----------+--------+
13
+ # | login_id | login | passwd |
14
+ # +----------+----------+--------+
15
+ # | 1 | brian | foobar |
16
+ # | 2 | caroline | boing |
17
+ # +----------+----------+--------+
18
+ #
19
+ # We support LDAP searches for (uid=login), returning a synthesised DN and
20
+ # Maildir attribute, and we support LDAP binds to validate passwords. We
21
+ # keep a cache of recent lookups so that a bind to validate a password
22
+ # doesn't cause a second SQL query. Since we're multi-threaded, this should
23
+ # work even if the bind occurs on a different client connection to the search.
24
+ #
25
+ # To test:
26
+ # ldapsearch -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(uid=brian)"
27
+ #
28
+ # ldapsearch -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" \
29
+ # -D "id=1,dc=example,dc=com" -W "(uid=brian)"
30
+
31
+ $debug = true
32
+ SQL_CONNECT = ["1.2.3.4", "myuser", "mypass", "mydb"]
33
+ TABLE = "logins"
34
+ SQL_POOL_SIZE = 5
35
+ PW_CACHE_SIZE = 100
36
+ BASEDN = "dc=example,dc=com"
37
+ LDAP_PORT = 1389
38
+
39
+ # A thread-safe pool of persistent MySQL connections
40
+
41
+ class SQLPool
42
+ def initialize(n, *args)
43
+ @args = args
44
+ @pool = Queue.new # this is a thread-safe queue
45
+ n.times { @pool.push nil } # create connections on demand
46
+ end
47
+
48
+ def borrow
49
+ conn = @pool.pop || Mysql::new(*@args)
50
+ yield conn
51
+ rescue Exception
52
+ conn = nil # put 'nil' back into the pool
53
+ raise
54
+ ensure
55
+ @pool.push conn
56
+ end
57
+ end
58
+
59
+ # An simple LRU cache of username->password. It's linearly searched
60
+ # so don't make it too big.
61
+
62
+ class LRUCache
63
+ def initialize(size)
64
+ @size = size
65
+ @cache = [] # [[key,val],[key,val],...]
66
+ @mutex = Mutex.new
67
+ end
68
+
69
+ def add(id,data)
70
+ @mutex.synchronize do
71
+ @cache.delete_if { |k,v| k == id }
72
+ @cache.unshift [id,data]
73
+ @cache.pop while @cache.size > @size
74
+ end
75
+ end
76
+
77
+ def find(id)
78
+ @mutex.synchronize do
79
+ index = entry = nil
80
+ @cache.each_with_index do |e, i|
81
+ if e[0] == id
82
+ entry = e
83
+ index = i
84
+ break
85
+ end
86
+ end
87
+ return nil unless index
88
+ @cache.delete_at(index)
89
+ @cache.unshift entry
90
+ return entry[1]
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+ class SQLOperation < LDAP::Server::Operation
97
+ def self.setcache(cache,pool)
98
+ @@cache = cache
99
+ @@pool = pool
100
+ end
101
+
102
+ # Handle searches of the form "(uid=<foo>)" using SQL backend
103
+ # (uid=foo) => [:eq, "uid", matchobj, "foo"]
104
+
105
+ def search(basedn, scope, deref, filter)
106
+ raise LDAP::ResultError::UnwillingToPerform, "Bad base DN" unless basedn == BASEDN
107
+ raise LDAP::ResultError::UnwillingToPerform, "Bad filter" unless filter[0..1] == [:eq, "uid"]
108
+ uid = filter[3]
109
+ @@pool.borrow do |sql|
110
+ q = "select login_id,passwd from #{TABLE} where login='#{sql.quote(uid)}'"
111
+ puts "SQL Query #{sql.object_id}: #{q}" if $debug
112
+ res = sql.query(q)
113
+ res.each do |login_id,passwd|
114
+ @@cache.add(login_id, passwd)
115
+ send_SearchResultEntry("id=#{login_id},#{BASEDN}", {
116
+ "maildir"=>["/netapp/#{uid}/"],
117
+ })
118
+ end
119
+ end
120
+ end
121
+
122
+ # Validate passwords
123
+
124
+ def simple_bind(version, dn, password)
125
+ return if dn.nil? # accept anonymous
126
+
127
+ raise LDAP::ResultError::UnwillingToPerform unless dn =~ /\Aid=(\d+),#{BASEDN}\z/
128
+ login_id = $1
129
+ dbpw = @@cache.find(login_id)
130
+ unless dbpw
131
+ @@pool.borrow do |sql|
132
+ q = "select passwd from #{TABLE} where login_id=#{login_id}"
133
+ puts "SQL Query #{sql.object_id}: #{q}" if $debug
134
+ res = sql.query(q)
135
+ if res.num_rows == 1
136
+ dbpw = res.fetch_row[0]
137
+ @@cache.add(login_id, dbpw)
138
+ end
139
+ end
140
+ end
141
+ raise LDAP::ResultError::InvalidCredentials unless dbpw and dbpw != "" and dbpw == password
142
+ end
143
+ end
144
+
145
+ # Build the objects we need
146
+
147
+ cache = LRUCache.new(PW_CACHE_SIZE)
148
+ pool = SQLPool.new(SQL_POOL_SIZE, *SQL_CONNECT)
149
+ SQLOperation.setcache(cache,pool)
150
+
151
+ s = LDAP::Server.new(
152
+ :port => LDAP_PORT,
153
+ :nodelay => true,
154
+ :listen => 10,
155
+ # :ssl_key_file => "key.pem",
156
+ # :ssl_cert_file => "cert.pem",
157
+ # :ssl_on_connect => true,
158
+ :operation_class => SQLOperation
159
+ )
160
+ s.run_tcpserver
161
+ s.join
@@ -0,0 +1,172 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # This is similar to rbslapd1.rb but here we use TOMITA Masahiro's prefork
4
+ # library: <http://raa.ruby-lang.org/project/prefork/>
5
+ # Advantages over Ruby threading:
6
+ # - each client connection is handled in its own process; don't need
7
+ # to worry about Ruby thread blocking (except if one client issues
8
+ # overlapping LDAP operations down the same connection, which is uncommon)
9
+ # - better scalability on multi-processor systems
10
+ # - better scalability on single-processor systems (e.g. shouldn't hit
11
+ # max FDs per process limit)
12
+ # Disadvantages:
13
+ # - client connections can't share state in RAM. So our shared directory
14
+ # now has to be read from disk, and flushed to disk after every update.
15
+ #
16
+ # Additionally, I have added schema support. An LDAP v3 client can
17
+ # query the schema remotely, and adds/modifies have data validated.
18
+
19
+ $:.unshift('../lib')
20
+
21
+ require 'ldap/server'
22
+ require 'ldap/server/schema'
23
+ require 'yaml'
24
+
25
+ $debug = nil # $stderr
26
+
27
+ # An object to keep our in-RAM database and synchronise it to disk
28
+ # when necessary
29
+
30
+ class Directory
31
+ attr_reader :data
32
+
33
+ def initialize(filename)
34
+ @filename = filename
35
+ @stat = nil
36
+ update
37
+ end
38
+
39
+ # synchronise with directory on disk (re-read if it has changed)
40
+
41
+ def update
42
+ begin
43
+ tmp = {}
44
+ sb = File.stat(@filename)
45
+ return if @stat and @stat.ino == sb.ino and @stat.mtime == sb.mtime
46
+ File.open(@filename) do |f|
47
+ tmp = YAML::load(f.read)
48
+ @stat = f.stat
49
+ end
50
+ rescue Errno::ENOENT
51
+ end
52
+ @data = tmp
53
+ end
54
+
55
+ # write back to disk
56
+
57
+ def write
58
+ File.open(@filename+".new","w") { |f| f.write(YAML::dump(@data)) }
59
+ File.rename(@filename+".new",@filename)
60
+ @stat = File.stat(@filename)
61
+ end
62
+
63
+ # run a block while holding a lock on the database
64
+
65
+ def lock
66
+ File.open(@filename+".lock","w") do |f|
67
+ f.flock(File::LOCK_EX) # will block here until lock available
68
+ yield
69
+ end
70
+ end
71
+ end
72
+
73
+ # We subclass the Operation class, overriding the methods to do what we need
74
+
75
+ class DirOperation < LDAP::Server::Operation
76
+ def initialize(connection, messageID, dir)
77
+ super(connection, messageID)
78
+ @dir = dir
79
+ end
80
+
81
+ def search(basedn, scope, deref, filter)
82
+ $debug << "Search: basedn=#{basedn.inspect}, scope=#{scope.inspect}, deref=#{deref.inspect}, filter=#{filter.inspect}\n" if $debug
83
+ basedn.downcase!
84
+
85
+ case scope
86
+ when LDAP::Server::BaseObject
87
+ # client asked for single object by DN
88
+ @dir.update
89
+ obj = @dir.data[basedn]
90
+ raise LDAP::ResultError::NoSuchObject unless obj
91
+ ok = LDAP::Server::Filter.run(filter, obj)
92
+ $debug << "Match=#{ok.inspect}: #{obj.inspect}\n" if $debug
93
+ send_SearchResultEntry(basedn, obj) if ok
94
+
95
+ when LDAP::Server::WholeSubtree
96
+ @dir.update
97
+ @dir.data.each do |dn, av|
98
+ $debug << "Considering #{dn}\n" if $debug
99
+ next unless dn.index(basedn, -basedn.length) # under basedn?
100
+ next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
101
+ $debug << "Sending: #{av.inspect}\n" if $debug
102
+ send_SearchResultEntry(dn, av)
103
+ end
104
+
105
+ else
106
+ raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
107
+
108
+ end
109
+ end
110
+
111
+ def add(dn, entry)
112
+ entry = @schema.validate(entry)
113
+ entry['createTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
114
+ entry['creatorsName'] = [@connection.binddn.to_s]
115
+ # FIXME: normalize the DN and check it's below our root DN
116
+ # FIXME: validate that a superior object exists
117
+ # FIXME: validate that entry contains the RDN attribute (yuk)
118
+ dn.downcase!
119
+ @dir.lock do
120
+ @dir.update
121
+ raise LDAP::ResultError::EntryAlreadyExists if @dir.data[dn]
122
+ @dir.data[dn] = entry
123
+ @dir.write
124
+ end
125
+ end
126
+
127
+ def del(dn)
128
+ dn.downcase!
129
+ @dir.lock do
130
+ @dir.update
131
+ raise LDAP::ResultError::NoSuchObject unless @dir.data.has_key?(dn)
132
+ @dir.data.delete(dn)
133
+ @dir.write
134
+ end
135
+ end
136
+
137
+ def modify(dn, ops)
138
+ dn.downcase!
139
+ @dir.lock do
140
+ @dir.update
141
+ entry = @dir.data[dn]
142
+ raise LDAP::ResultError::NoSuchObject unless entry
143
+ entry = @schema.validate(ops, entry) # also does the update
144
+ entry['modifyTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
145
+ entry['modifiersName'] = [@connection.binddn.to_s]
146
+ @dir.data[dn] = entry
147
+ @dir.write
148
+ end
149
+ end
150
+ end
151
+
152
+ directory = Directory.new("ldapdb.yaml")
153
+
154
+ schema = LDAP::Server::Schema.new
155
+ schema.load_system
156
+ schema.load_file("../test/core.schema")
157
+ schema.resolve_oids
158
+
159
+ s = LDAP::Server.new(
160
+ :port => 1389,
161
+ :nodelay => true,
162
+ :listen => 10,
163
+ # :ssl_key_file => "key.pem",
164
+ # :ssl_cert_file => "cert.pem",
165
+ # :ssl_on_connect => true,
166
+ :operation_class => DirOperation,
167
+ :operation_args => [directory],
168
+ :schema => schema,
169
+ :namingContexts => ['dc=example,dc=com']
170
+ )
171
+ s.run_prefork
172
+ s.join