#!/usr/bin/env ruby # 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 = false require 'ldap/server' require 'ldap/server/router' $logger = Logger.new($stderr) class HashOperation < LDAP::Server::Operation def initialize super @hash = YAML.load_from_file('ldapdbp.yaml') # 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 end class LDAPController def initialize @directory = {} File.open('ldapdb.yaml') { |f| @directory = YAML.load(f.read) } end 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) user = params[:uid] domain = params[:domain] tld = params[:tld] # p "bindUser user=#{user} dn=#{dn}, password=#{password}, params=#{params}" if user.length < 2 $logger.warn "Denied access for user #{user}: Size < 2" raise LDAP::ResultError::InvalidCredentials, 'Invalid credentials' end $logger.info "Authenticated email=#{user}@#{domain}.#{tld} with password=" end def self.searchUsers(request, baseObject, scope, deref, filter, params) $logger.info 'Search users' domain = params[:domain] tld = params[:tld] basedn = "#{domain}.#{tld}" operation = HashOperation.new case scope when LDAP::Server::BaseObject # client asked for single object by DN obj = directory raise LDAP::ResultError::NoSuchObject unless obj operation.send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj) when LDAP::Server::WholeSubtree directory.each do |dn, av| next unless dn.index(basedn, -basedn.length) # under basedn? next unless LDAP::Server::Filter.run(filter, av) # attribute filter? operation.send_SearchResultEntry(dn, av) end else raise LDAP::ResultError::UnwillingToPerform, 'OneLevel not implemented' end 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' bind 'uid=:uid,dc=:domain,dc=:tld' => 'LDAPController#bindUser' search 'dc=:domain,dc=:tld' => 'LDAPController#searchUsers' # 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