init
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
Using the example programs
|
||||
==========================
|
||||
|
||||
These servers all listen on port 1389 by default, so that they don't have to
|
||||
be run as root.
|
||||
|
||||
Example 1: trivial server using RAM hash
|
||||
----------------------------------------
|
||||
|
||||
$ ruby rbslapd1.rb
|
||||
|
||||
In another window:
|
||||
|
||||
$ ldapadd -x -H ldap://127.0.0.1:1389/
|
||||
dn: dc=example,dc=com
|
||||
cn: Top object
|
||||
|
||||
dn: cn=Fred Flintstone,dc=example,dc=com
|
||||
cn: Fred Flintstone
|
||||
sn: Flintstone
|
||||
mail: fred@bedrock.org
|
||||
mail: fred.flintstone@bedrock.org
|
||||
|
||||
dn: cn=Wilma Flintstone,dc=example,dc=com
|
||||
cn: Wilma Flintstone
|
||||
mail: wilma@bedrock.org
|
||||
^D
|
||||
|
||||
Try these queries:
|
||||
|
||||
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "" "(objectclass=*)"
|
||||
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" -s base "(objectclass=*)"
|
||||
$ ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(mail=fred*)"
|
||||
|
||||
If you terminate the server with Ctrl-C, its contents should be written
|
||||
to disk as a YAML file.
|
||||
|
||||
A fairly complete set of the filter language is implemented. However, this
|
||||
simple server works by simply scanning the entire database and applying the
|
||||
filter to each entry, so it won't scale to large applications. No validation
|
||||
of DN or attributes against any sort of schema is done.
|
||||
|
||||
Example 1a: with SSL
|
||||
--------------------
|
||||
|
||||
In rbslapd1.rb, uncomment
|
||||
|
||||
:ssl_key_file => "key.pem",
|
||||
:ssl_cert_file => "cert.pem",
|
||||
:ssl_on_connect => true,
|
||||
|
||||
and run mkcert.rb. Since this is a self-signed certificate, you'll have to
|
||||
turn off certificate verification in the client too. For example:
|
||||
|
||||
$ env LDAPTLS_REQCERT="allow" ldapsearch -x -H ldaps://127.0.0.1:1389/
|
||||
|
||||
Making your own CA and installing its certificate in the client, or
|
||||
generating a Certificate Signing Request and sending it to a known CA, is
|
||||
beyond the scope of this documentation.
|
||||
|
||||
Example 2: simple LDAP to SQL mapping
|
||||
-------------------------------------
|
||||
|
||||
You will need to set up a MySQL database with a table conforming to the
|
||||
schema given within the code. Once done, LDAP gives a read-only view of the
|
||||
database with only the filter "(uid=<foo>)" supported.
|
||||
|
||||
Example 3: preforking server and schema
|
||||
---------------------------------------
|
||||
|
||||
This functions in the same way as rbslapd1.rb. However, since each query is
|
||||
answered in a separate process, the YAML file on disk is used as the master
|
||||
repository. Update operations re-write this file each time.
|
||||
|
||||
Also, the schema is read from file 'core.schema'. Attempting to insert the
|
||||
above entries will fail, due to schema violations. Insert a valid entry,
|
||||
e.g.
|
||||
|
||||
dn: cn=Fred Flintstone,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
cn: Fred Flintstone
|
||||
sn: Flintstone
|
||||
telephoneNumber: +1 555 1234
|
||||
telephoneNumber: +1 555 5432
|
||||
|
||||
Schema validation takes place for the attribute values and that attributes
|
||||
are allowed/required by the objectclass(es); however, the DN itself is not
|
||||
validated, nor any checks made that the RDN is present as an attribute
|
||||
(since this is one of the more stupid parts of the LDAP/X500 data model)
|
||||
|
||||
|
||||
Example 4
|
||||
------------
|
||||
|
||||
* ruby rbslapd4.rb
|
||||
* `ldapwhoami -x -H ldap://127.0.0.1:1389 -D "uid=admin,ou=Users,dc=mydomain,dc=com" -w "adminpassword"`
|
||||
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
Q34692xrylRsx0qRw37Rk525ihmim+LLlw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
dc=example,dc=com:
|
||||
cn:
|
||||
- Top object
|
||||
cn=fred flintstone,dc=example,dc=com:
|
||||
cn:
|
||||
- Fred Flintstone
|
||||
sn:
|
||||
- Flintstone
|
||||
mail:
|
||||
- fred@bedrock.org
|
||||
- fred.flintstone@bedrock.org
|
||||
cn=wilma flintstone,dc=example,dc=com:
|
||||
cn:
|
||||
- Wilma Flintstone
|
||||
mail:
|
||||
- wilma@bedrock.org
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'openssl'
|
||||
|
||||
# Taken directly from echo_svr.rb in the Ruby openssl examples
|
||||
|
||||
key = OpenSSL::PKey::RSA.new(4096) do
|
||||
print '.'
|
||||
$stdout.flush
|
||||
end
|
||||
puts
|
||||
cert = OpenSSL::X509::Certificate.new
|
||||
cert.version = 2
|
||||
cert.serial = 0
|
||||
name = OpenSSL::X509::Name.new([%w[C JP], %w[O TEST], %w[CN localhost]])
|
||||
cert.subject = name
|
||||
cert.issuer = name
|
||||
cert.not_before = Time.now
|
||||
cert.not_after = Time.now + 3600
|
||||
cert.public_key = key.public_key
|
||||
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
|
||||
cert.extensions = [
|
||||
ef.create_extension('basicConstraints', 'CA:FALSE'),
|
||||
ef.create_extension('subjectKeyIdentifier', 'hash'),
|
||||
ef.create_extension('extendedKeyUsage', 'serverAuth'),
|
||||
ef.create_extension('keyUsage',
|
||||
'keyEncipherment,dataEncipherment,digitalSignature')
|
||||
]
|
||||
ef.issuer_certificate = cert
|
||||
cert.add_extension ef.create_extension('authorityKeyIdentifier',
|
||||
'keyid:always,issuer:always')
|
||||
cert.sign(key, OpenSSL::Digest.new('SHA1'))
|
||||
|
||||
# Write to disk
|
||||
File.open('key.pem', 'w', 0o600) { |f| f << key.to_pem }
|
||||
File.open('cert.pem', 'w', 0o644) { |f| f << cert.to_pem }
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/local/bin/ruby -w
|
||||
|
||||
# This is a trivial LDAP server which just stores directory entries in RAM.
|
||||
# It does no validation or authentication. This is intended just to
|
||||
# demonstrate the API, it's not for real-world use!!
|
||||
|
||||
$:.unshift('../lib')
|
||||
$debug = true
|
||||
|
||||
require 'ldap/server'
|
||||
|
||||
# We subclass the Operation class, overriding the methods to do what we need
|
||||
|
||||
class HashOperation < LDAP::Server::Operation
|
||||
def initialize(connection, messageID, hash)
|
||||
super(connection, messageID)
|
||||
@hash = hash # an object reference to our directory data
|
||||
end
|
||||
|
||||
def search(basedn, scope, deref, filter)
|
||||
basedn = basedn.downcase
|
||||
|
||||
case scope
|
||||
when LDAP::Server::BaseObject
|
||||
# client asked for single object by DN
|
||||
obj = @hash[basedn]
|
||||
raise LDAP::ResultError::NoSuchObject unless obj
|
||||
send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj)
|
||||
|
||||
when LDAP::Server::WholeSubtree
|
||||
@hash.each do |dn, av|
|
||||
next unless dn.index(basedn, -basedn.length) # under basedn?
|
||||
next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
|
||||
send_SearchResultEntry(dn, av)
|
||||
end
|
||||
|
||||
else
|
||||
raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def add(dn, av)
|
||||
dn = dn.downcase
|
||||
raise LDAP::ResultError::EntryAlreadyExists if @hash[dn]
|
||||
@hash[dn] = av
|
||||
end
|
||||
|
||||
def del(dn)
|
||||
dn = dn.downcase
|
||||
raise LDAP::ResultError::NoSuchObject unless @hash.has_key?(dn)
|
||||
@hash.delete(dn)
|
||||
end
|
||||
|
||||
def modify(dn, ops)
|
||||
dn = dn.downcase
|
||||
entry = @hash[dn]
|
||||
raise LDAP::ResultError::NoSuchObject unless entry
|
||||
ops.each do |attr, vals|
|
||||
op = vals.shift
|
||||
case op
|
||||
when :add
|
||||
entry[attr] ||= []
|
||||
entry[attr] += vals
|
||||
entry[attr].uniq!
|
||||
when :delete
|
||||
if vals == []
|
||||
entry.delete(attr)
|
||||
else
|
||||
vals.each { |v| entry[attr].delete(v) }
|
||||
end
|
||||
when :replace
|
||||
entry[attr] = vals
|
||||
end
|
||||
entry.delete(attr) if entry[attr] == []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is the shared object which carries our actual directory entries.
|
||||
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
|
||||
|
||||
directory = {}
|
||||
|
||||
# Let's put some backing store on it
|
||||
|
||||
require 'yaml'
|
||||
begin
|
||||
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
|
||||
rescue Errno::ENOENT
|
||||
end
|
||||
|
||||
at_exit do
|
||||
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
|
||||
File.rename("ldapdb.new","ldapdb.yaml")
|
||||
end
|
||||
|
||||
# Listen for incoming LDAP connections. For each one, create a Connection
|
||||
# object, which will invoke a HashOperation object for each request.
|
||||
|
||||
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 => HashOperation,
|
||||
:operation_args => [directory]
|
||||
)
|
||||
s.run_tcpserver
|
||||
s.join
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/local/bin/ruby -w
|
||||
|
||||
$:.unshift('../lib')
|
||||
require 'ldap/server'
|
||||
require 'mysql' # <http://www.tmtm.org/en/ruby/mysql/>
|
||||
require 'thread'
|
||||
require 'resolv-replace' # ruby threading DNS client
|
||||
|
||||
# An example of an LDAP to SQL gateway. We have a MySQL table which
|
||||
# contains (login_id,login,passwd) combinations, e.g.
|
||||
#
|
||||
# +----------+----------+--------+
|
||||
# | login_id | login | passwd |
|
||||
# +----------+----------+--------+
|
||||
# | 1 | brian | foobar |
|
||||
# | 2 | caroline | boing |
|
||||
# +----------+----------+--------+
|
||||
#
|
||||
# We support LDAP searches for (uid=login), returning a synthesised DN and
|
||||
# Maildir attribute, and we support LDAP binds to validate passwords. We
|
||||
# keep a cache of recent lookups so that a bind to validate a password
|
||||
# doesn't cause a second SQL query. Since we're multi-threaded, this should
|
||||
# work even if the bind occurs on a different client connection to the search.
|
||||
#
|
||||
# To test:
|
||||
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(uid=brian)"
|
||||
#
|
||||
# ldapsearch -x -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" \
|
||||
# -D "id=1,dc=example,dc=com" -W "(uid=brian)"
|
||||
|
||||
$debug = true
|
||||
SQL_CONNECT = ["1.2.3.4", "myuser", "mypass", "mydb"]
|
||||
TABLE = "logins"
|
||||
SQL_POOL_SIZE = 5
|
||||
PW_CACHE_SIZE = 100
|
||||
BASEDN = "dc=example,dc=com"
|
||||
LDAP_PORT = 1389
|
||||
|
||||
# A thread-safe pool of persistent MySQL connections
|
||||
|
||||
class SQLPool
|
||||
def initialize(n, *args)
|
||||
@args = args
|
||||
@pool = Queue.new # this is a thread-safe queue
|
||||
n.times { @pool.push nil } # create connections on demand
|
||||
end
|
||||
|
||||
def borrow
|
||||
conn = @pool.pop || Mysql::new(*@args)
|
||||
yield conn
|
||||
rescue Exception
|
||||
conn = nil # put 'nil' back into the pool
|
||||
raise
|
||||
ensure
|
||||
@pool.push conn
|
||||
end
|
||||
end
|
||||
|
||||
# An simple LRU cache of username->password. It's linearly searched
|
||||
# so don't make it too big.
|
||||
|
||||
class LRUCache
|
||||
def initialize(size)
|
||||
@size = size
|
||||
@cache = [] # [[key,val],[key,val],...]
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def add(id,data)
|
||||
@mutex.synchronize do
|
||||
@cache.delete_if { |k,v| k == id }
|
||||
@cache.unshift [id,data]
|
||||
@cache.pop while @cache.size > @size
|
||||
end
|
||||
end
|
||||
|
||||
def find(id)
|
||||
@mutex.synchronize do
|
||||
index = entry = nil
|
||||
@cache.each_with_index do |e, i|
|
||||
if e[0] == id
|
||||
entry = e
|
||||
index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
return nil unless index
|
||||
@cache.delete_at(index)
|
||||
@cache.unshift entry
|
||||
return entry[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class SQLOperation < LDAP::Server::Operation
|
||||
def self.setcache(cache,pool)
|
||||
@@cache = cache
|
||||
@@pool = pool
|
||||
end
|
||||
|
||||
# Handle searches of the form "(uid=<foo>)" using SQL backend
|
||||
# (uid=foo) => [:eq, "uid", matchobj, "foo"]
|
||||
|
||||
def search(basedn, scope, deref, filter)
|
||||
raise LDAP::ResultError::UnwillingToPerform, "Bad base DN" unless basedn == BASEDN
|
||||
raise LDAP::ResultError::UnwillingToPerform, "Bad filter" unless filter[0..1] == [:eq, "uid"]
|
||||
uid = filter[3]
|
||||
@@pool.borrow do |sql|
|
||||
q = "select login_id,passwd from #{TABLE} where login='#{sql.quote(uid)}'"
|
||||
puts "SQL Query #{sql.object_id}: #{q}" if $debug
|
||||
res = sql.query(q)
|
||||
res.each do |login_id,passwd|
|
||||
@@cache.add(login_id, passwd)
|
||||
send_SearchResultEntry("id=#{login_id},#{BASEDN}", {
|
||||
"maildir"=>["/netapp/#{uid}/"],
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validate passwords
|
||||
|
||||
def simple_bind(version, dn, password)
|
||||
return if dn.nil? # accept anonymous
|
||||
|
||||
raise LDAP::ResultError::UnwillingToPerform unless dn =~ /\Aid=(\d+),#{BASEDN}\z/
|
||||
login_id = $1
|
||||
dbpw = @@cache.find(login_id)
|
||||
unless dbpw
|
||||
@@pool.borrow do |sql|
|
||||
q = "select passwd from #{TABLE} where login_id=#{login_id}"
|
||||
puts "SQL Query #{sql.object_id}: #{q}" if $debug
|
||||
res = sql.query(q)
|
||||
if res.num_rows == 1
|
||||
dbpw = res.fetch_row[0]
|
||||
@@cache.add(login_id, dbpw)
|
||||
end
|
||||
end
|
||||
end
|
||||
raise LDAP::ResultError::InvalidCredentials unless dbpw and dbpw != "" and dbpw == password
|
||||
end
|
||||
end
|
||||
|
||||
# Build the objects we need
|
||||
|
||||
cache = LRUCache.new(PW_CACHE_SIZE)
|
||||
pool = SQLPool.new(SQL_POOL_SIZE, *SQL_CONNECT)
|
||||
SQLOperation.setcache(cache,pool)
|
||||
|
||||
s = LDAP::Server.new(
|
||||
:port => LDAP_PORT,
|
||||
:nodelay => true,
|
||||
:listen => 10,
|
||||
# :ssl_key_file => "key.pem",
|
||||
# :ssl_cert_file => "cert.pem",
|
||||
# :ssl_on_connect => true,
|
||||
:operation_class => SQLOperation
|
||||
)
|
||||
s.run_tcpserver
|
||||
s.join
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE logins (
|
||||
login_id MEDIUMINT NOT NULL AUTO_INCREMENT,
|
||||
login CHAR(30) NOT NULL,
|
||||
passwd CHAR(30),
|
||||
PRIMARY KEY (login_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
INSERT INTO logins(login, passwd) VALUES
|
||||
('brian', 'foobar'), ('caroline', 'boing');
|
||||
|
||||
SELECT * FROM logins;
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/local/bin/ruby -w
|
||||
|
||||
# This is a modified version of rbslapd1.rb which uses a Router instead of
|
||||
# subclassing the LDAP::Server::Operation class.
|
||||
|
||||
# This is a trivial LDAP server which just stores directory entries in RAM.
|
||||
# It does no validation or authentication. This is intended just to
|
||||
# demonstrate the API, it's not for real-world use!!
|
||||
|
||||
$:.unshift('../lib')
|
||||
$debug = true
|
||||
|
||||
require 'ldap/server'
|
||||
require 'ldap/server/router'
|
||||
|
||||
$logger = Logger.new($stderr)
|
||||
|
||||
class LDAPController
|
||||
def self.bind(request, version, dn, password, params)
|
||||
$logger.debug "Catchall bind request"
|
||||
raise LDAP::ResultError::UnwillingToPerform, "Invalid bind DN"
|
||||
end
|
||||
|
||||
def self.bindUser(request, version, dn, password, params)
|
||||
if params[:uid].nil? or
|
||||
params[:uid] != 'admin' or
|
||||
password != 'adminpassword'
|
||||
$logger.warn "Denied access for user #{params[:uid]}: Invalid credentials"
|
||||
raise LDAP::ResultError::InvalidCredentials, "Invalid credentials"
|
||||
end
|
||||
|
||||
$logger.info "Authenticated user #{params[:uid]}"
|
||||
end
|
||||
|
||||
def self.search(request, baseObject, scope, deref, filter, params)
|
||||
$logger.info "Catchall search request for #{baseObject}"
|
||||
raise LDAP::ResultError::UnwillingToPerform, "Invalid search DN"
|
||||
end
|
||||
|
||||
def self.searchUsers(request, baseObject, scope, deref, filter, params)
|
||||
$logger.info "Search users"
|
||||
end
|
||||
end
|
||||
|
||||
router = LDAP::Server::Router.new($logger) do
|
||||
# Different syntax but same thing
|
||||
bind nil => "LDAPController#bind"
|
||||
route :bind, nil => "LDAPController#bind"
|
||||
|
||||
# Bind a route using variables. A hash with the variables will be passed
|
||||
# to your function as last argument.
|
||||
bind "uid=:uid,ou=Users,dc=mydomain,dc=com" => "LDAPController#bindUser"
|
||||
|
||||
search nil => "LDAPController#search"
|
||||
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#searchUsers"
|
||||
end
|
||||
|
||||
|
||||
# This is the shared object which carries our actual directory entries.
|
||||
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
|
||||
|
||||
directory = {}
|
||||
|
||||
# Let's put some backing store on it
|
||||
|
||||
require 'yaml'
|
||||
begin
|
||||
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
|
||||
rescue Errno::ENOENT
|
||||
end
|
||||
|
||||
at_exit do
|
||||
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
|
||||
File.rename("ldapdb.new","ldapdb.yaml")
|
||||
end
|
||||
|
||||
# Listen for incoming LDAP connections. For each one, create a Connection
|
||||
# object, which will invoke a HashOperation object for each request.
|
||||
|
||||
s = LDAP::Server.new(
|
||||
:port => 1389,
|
||||
:nodelay => true,
|
||||
:listen => 10,
|
||||
# :ssl_key_file => "key.pem",
|
||||
# :ssl_cert_file => "cert.pem",
|
||||
# :ssl_on_connect => true,
|
||||
:router => router
|
||||
)
|
||||
s.run_tcpserver
|
||||
s.join
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/local/bin/ruby -w
|
||||
|
||||
# Example server that listens on both a port and a UNIX domain socket
|
||||
# Try it using:
|
||||
# $ ldapsearch -LLL -H ldap://localhost:1389 -D uid=whatever -b ou=Users,dc=mydomain,dc=com
|
||||
# $ ldapsearch -LLL -H ldapi://%2ftmp%2frbslapd5.sock -D uid=whatever -b ou=Users,dc=mydomain,dc=com
|
||||
|
||||
$:.unshift('../lib')
|
||||
$debug = true
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
require 'ldap/server'
|
||||
require 'ldap/server/router'
|
||||
|
||||
$logger = Logger.new($stderr)
|
||||
|
||||
class LDAPController
|
||||
def self.bind(request, version, dn, password, params)
|
||||
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'"
|
||||
end
|
||||
|
||||
def self.search(request, baseObject, scope, deref, filter, params)
|
||||
$logger.info "Processing search route for #{baseObject}"
|
||||
|
||||
h = {
|
||||
'uid' => 'jdoe',
|
||||
'objectClass' => 'userAccount',
|
||||
'givenName' => 'John',
|
||||
'sn' => 'Doe'
|
||||
}
|
||||
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h)
|
||||
end
|
||||
end
|
||||
|
||||
router = LDAP::Server::Router.new($logger) do
|
||||
bind nil => "LDAPController#bind"
|
||||
|
||||
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search"
|
||||
end
|
||||
|
||||
params = {
|
||||
:nodelay => true,
|
||||
:listen => 10,
|
||||
:router => router
|
||||
}
|
||||
|
||||
# Listen on IP address and port
|
||||
|
||||
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0
|
||||
params[:port] = 1389
|
||||
|
||||
addr_server = LDAP::Server.new params
|
||||
addr_server.run_tcpserver
|
||||
|
||||
# Listen on socket
|
||||
|
||||
params.delete :bindaddr
|
||||
params.delete :port
|
||||
params[:socket] = '/tmp/rbslapd5.sock'
|
||||
|
||||
FileUtils::rm_f params[:socket]
|
||||
|
||||
socket_server = LDAP::Server.new params
|
||||
socket_server.run_tcpserver
|
||||
|
||||
trap 'INT' do
|
||||
addr_server.stop
|
||||
socket_server.stop
|
||||
end
|
||||
|
||||
addr_server.join
|
||||
socket_server.join
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/local/bin/ruby -w
|
||||
|
||||
# Slightly modified version of rbslapd5.rb which demonstrates dropping
|
||||
# root privileges after binding to port 389
|
||||
#
|
||||
# Run this script with `sudo`
|
||||
|
||||
$:.unshift('../lib')
|
||||
$debug = true
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
require 'ldap/server'
|
||||
require 'ldap/server/router'
|
||||
|
||||
$logger = Logger.new($stderr)
|
||||
|
||||
class LDAPController
|
||||
def self.bind(request, version, dn, password, params)
|
||||
$logger.info "Processing bind route for \'#{dn}\' with password \'#{password}\'"
|
||||
end
|
||||
|
||||
def self.search(request, baseObject, scope, deref, filter, params)
|
||||
$logger.info "Processing search route for #{baseObject}"
|
||||
|
||||
h = {
|
||||
'uid' => 'jdoe',
|
||||
'objectClass' => 'userAccount',
|
||||
'givenName' => 'John',
|
||||
'sn' => 'Doe'
|
||||
}
|
||||
request.send_SearchResultEntry("uid=jdoe,#{baseObject}", h)
|
||||
end
|
||||
end
|
||||
|
||||
router = LDAP::Server::Router.new($logger) do
|
||||
bind nil => "LDAPController#bind"
|
||||
|
||||
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#search"
|
||||
end
|
||||
|
||||
params = {
|
||||
:nodelay => true,
|
||||
:listen => 10,
|
||||
:router => router
|
||||
}
|
||||
|
||||
# Listen on IP address and port
|
||||
|
||||
params[:bindaddr] = '127.0.0.1' # Leave this blank to listen on 0.0.0.0
|
||||
params[:port] = 389
|
||||
params[:user] = 'ldap'
|
||||
params[:group] = 'ldap'
|
||||
|
||||
addr_server = LDAP::Server.new params
|
||||
addr_server.run_tcpserver
|
||||
|
||||
# Listen on socket
|
||||
|
||||
params.delete :bindaddr
|
||||
params.delete :port
|
||||
params[:socket] = '/tmp/rbslapd6.sock'
|
||||
|
||||
FileUtils::rm_f params[:socket]
|
||||
|
||||
socket_server = LDAP::Server.new params
|
||||
socket_server.run_tcpserver
|
||||
|
||||
trap 'INT' do
|
||||
addr_server.stop
|
||||
socket_server.stop
|
||||
end
|
||||
|
||||
addr_server.join
|
||||
socket_server.join
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/local/bin/ruby
|
||||
|
||||
require 'ldap'
|
||||
|
||||
CHILDREN = 10
|
||||
CONNECTS = 1 # per child
|
||||
SEARCHES = 100 # per connection
|
||||
|
||||
pids = []
|
||||
CHILDREN.times do
|
||||
pids << fork do
|
||||
CONNECTS.times do
|
||||
conn = LDAP::Conn.new("localhost",1389)
|
||||
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
|
||||
conn.bind
|
||||
SEARCHES.times do
|
||||
res = conn.search("cn=Fred Flintstone,dc=example,dc=com", LDAP::LDAP_SCOPE_BASE,
|
||||
"(objectclass=*)") do |e|
|
||||
#puts "#{$$} #{e.dn.inspect}"
|
||||
end
|
||||
end
|
||||
conn.unbind
|
||||
end
|
||||
end
|
||||
end
|
||||
okcount = 0
|
||||
badcount = 0
|
||||
pids.each do |p|
|
||||
Process.wait(p)
|
||||
if $?.exitstatus == 0
|
||||
okcount += 1
|
||||
else
|
||||
badcount += 1
|
||||
end
|
||||
end
|
||||
puts "Children finished: #{okcount} ok, #{badcount} failed"
|
||||
exit badcount > 0 ? 1 : 0
|
||||
Reference in New Issue
Block a user