ruby-ldapserver 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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