You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
172 lines
4.8 KiB
172 lines
4.8 KiB
#!/usr/local/bin/ruby -w
|
|
|
|
# This is similar to rbslapd1.rb but here we use TOMITA Masahiro's prefork
|
|
# library: <http://raa.ruby-lang.org/project/prefork/>
|
|
# Advantages over Ruby threading:
|
|
# - each client connection is handled in its own process; don't need
|
|
# to worry about Ruby thread blocking (except if one client issues
|
|
# overlapping LDAP operations down the same connection, which is uncommon)
|
|
# - better scalability on multi-processor systems
|
|
# - better scalability on single-processor systems (e.g. shouldn't hit
|
|
# max FDs per process limit)
|
|
# Disadvantages:
|
|
# - client connections can't share state in RAM. So our shared directory
|
|
# now has to be read from disk, and flushed to disk after every update.
|
|
#
|
|
# Additionally, I have added schema support. An LDAP v3 client can
|
|
# query the schema remotely, and adds/modifies have data validated.
|
|
|
|
$:.unshift('../lib')
|
|
|
|
require 'ldap/server'
|
|
require 'ldap/server/schema'
|
|
require 'yaml'
|
|
|
|
$debug = nil # $stderr
|
|
|
|
# An object to keep our in-RAM database and synchronise it to disk
|
|
# when necessary
|
|
|
|
class Directory
|
|
attr_reader :data
|
|
|
|
def initialize(filename)
|
|
@filename = filename
|
|
@stat = nil
|
|
update
|
|
end
|
|
|
|
# synchronise with directory on disk (re-read if it has changed)
|
|
|
|
def update
|
|
begin
|
|
tmp = {}
|
|
sb = File.stat(@filename)
|
|
return if @stat and @stat.ino == sb.ino and @stat.mtime == sb.mtime
|
|
File.open(@filename) do |f|
|
|
tmp = YAML::load(f.read)
|
|
@stat = f.stat
|
|
end
|
|
rescue Errno::ENOENT
|
|
end
|
|
@data = tmp
|
|
end
|
|
|
|
# write back to disk
|
|
|
|
def write
|
|
File.open(@filename+".new","w") { |f| f.write(YAML::dump(@data)) }
|
|
File.rename(@filename+".new",@filename)
|
|
@stat = File.stat(@filename)
|
|
end
|
|
|
|
# run a block while holding a lock on the database
|
|
|
|
def lock
|
|
File.open(@filename+".lock","w") do |f|
|
|
f.flock(File::LOCK_EX) # will block here until lock available
|
|
yield
|
|
end
|
|
end
|
|
end
|
|
|
|
# We subclass the Operation class, overriding the methods to do what we need
|
|
|
|
class DirOperation < LDAP::Server::Operation
|
|
def initialize(connection, messageID, dir)
|
|
super(connection, messageID)
|
|
@dir = dir
|
|
end
|
|
|
|
def search(basedn, scope, deref, filter)
|
|
$debug << "Search: basedn=#{basedn.inspect}, scope=#{scope.inspect}, deref=#{deref.inspect}, filter=#{filter.inspect}\n" if $debug
|
|
basedn = basedn.downcase
|
|
|
|
case scope
|
|
when LDAP::Server::BaseObject
|
|
# client asked for single object by DN
|
|
@dir.update
|
|
obj = @dir.data[basedn]
|
|
raise LDAP::ResultError::NoSuchObject unless obj
|
|
ok = LDAP::Server::Filter.run(filter, obj)
|
|
$debug << "Match=#{ok.inspect}: #{obj.inspect}\n" if $debug
|
|
send_SearchResultEntry(basedn, obj) if ok
|
|
|
|
when LDAP::Server::WholeSubtree
|
|
@dir.update
|
|
@dir.data.each do |dn, av|
|
|
$debug << "Considering #{dn}\n" if $debug
|
|
next unless dn.index(basedn, -basedn.length) # under basedn?
|
|
next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
|
|
$debug << "Sending: #{av.inspect}\n" if $debug
|
|
send_SearchResultEntry(dn, av)
|
|
end
|
|
|
|
else
|
|
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
|
|
|
|
end
|
|
end
|
|
|
|
def add(dn, entry)
|
|
entry = @schema.validate(entry)
|
|
entry['createTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
|
|
entry['creatorsName'] = [@connection.binddn.to_s]
|
|
# FIXME: normalize the DN and check it's below our root DN
|
|
# FIXME: validate that a superior object exists
|
|
# FIXME: validate that entry contains the RDN attribute (yuk)
|
|
dn = dn.downcase
|
|
@dir.lock do
|
|
@dir.update
|
|
raise LDAP::ResultError::EntryAlreadyExists if @dir.data[dn]
|
|
@dir.data[dn] = entry
|
|
@dir.write
|
|
end
|
|
end
|
|
|
|
def del(dn)
|
|
dn = dn.downcase
|
|
@dir.lock do
|
|
@dir.update
|
|
raise LDAP::ResultError::NoSuchObject unless @dir.data.has_key?(dn)
|
|
@dir.data.delete(dn)
|
|
@dir.write
|
|
end
|
|
end
|
|
|
|
def modify(dn, ops)
|
|
dn = dn.downcase
|
|
@dir.lock do
|
|
@dir.update
|
|
entry = @dir.data[dn]
|
|
raise LDAP::ResultError::NoSuchObject unless entry
|
|
entry = @schema.validate(ops, entry) # also does the update
|
|
entry['modifyTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
|
|
entry['modifiersName'] = [@connection.binddn.to_s]
|
|
@dir.data[dn] = entry
|
|
@dir.write
|
|
end
|
|
end
|
|
end
|
|
|
|
directory = Directory.new("ldapdb.yaml")
|
|
|
|
schema = LDAP::Server::Schema.new
|
|
schema.load_system
|
|
schema.load_file("../test/core.schema")
|
|
schema.resolve_oids
|
|
|
|
s = LDAP::Server.new(
|
|
:port => 1389,
|
|
:nodelay => true,
|
|
:listen => 10,
|
|
# :ssl_key_file => "key.pem",
|
|
# :ssl_cert_file => "cert.pem",
|
|
# :ssl_on_connect => true,
|
|
:operation_class => DirOperation,
|
|
:operation_args => [directory],
|
|
:schema => schema,
|
|
:namingContexts => ['dc=example,dc=com']
|
|
)
|
|
s.run_prefork
|
|
s.join
|