fakeldap 0.0.0
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.
- data/LICENSE +20 -0
- data/README.md +3 -0
- data/lib/fakeldap.rb +46 -0
- data/lib/fakeldap/version.rb +4 -0
- data/vendor/ruby-ldapserver/COPYING +27 -0
- data/vendor/ruby-ldapserver/ChangeLog +83 -0
- data/vendor/ruby-ldapserver/Manifest.txt +32 -0
- data/vendor/ruby-ldapserver/README +222 -0
- data/vendor/ruby-ldapserver/Rakefile +22 -0
- data/vendor/ruby-ldapserver/examples/README +89 -0
- data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
- data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
- data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
- data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
- data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
- data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
- data/vendor/ruby-ldapserver/test/core.schema +582 -0
- data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
- data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
- data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
- data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
- data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
- data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
- data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
- 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
|