fakeldap 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|