client authentication from any domain
This commit is contained in:
@@ -1,5 +1,20 @@
|
|||||||
# ruby-ldapserver
|
# ruby-ldapserver
|
||||||
|
|
||||||
|
## ZOURIT-ADMIN
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
* gem install simple_ldap_authenticator net-ldap
|
||||||
|
|
||||||
|
# server
|
||||||
|
* ruby rbslapd4.rb
|
||||||
|
|
||||||
|
# client
|
||||||
|
* ruby client.rb
|
||||||
|
|
||||||
|
|
||||||
|
---------------
|
||||||
|
|
||||||
|
|
||||||
ruby-ldapserver is a lightweight, pure Ruby framework for implementing LDAP server applications. It is intended primarily for building a gateway from LDAP queries into some other protocol or database. It does not attempt to be a full or correct implementation of the standard LDAP data model itself (although you could build one using this as a frontend).
|
ruby-ldapserver is a lightweight, pure Ruby framework for implementing LDAP server applications. It is intended primarily for building a gateway from LDAP queries into some other protocol or database. It does not attempt to be a full or correct implementation of the standard LDAP data model itself (although you could build one using this as a frontend).
|
||||||
|
|
||||||
Since it's written entirely in Ruby, it benefits from Ruby's threading engine.
|
Since it's written entirely in Ruby, it benefits from Ruby's threading engine.
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
require 'simple_ldap_authenticator'
|
||||||
|
|
||||||
|
DOMAIN = 'artcode'
|
||||||
|
TLD = 're'
|
||||||
|
|
||||||
|
SimpleLdapAuthenticator.servers = ['127.0.0.1']
|
||||||
|
SimpleLdapAuthenticator.port = 1389
|
||||||
|
SimpleLdapAuthenticator.use_ssl = false
|
||||||
|
# SimpleLdapAuthenticator.login_format = '%s@mydomain.com'
|
||||||
|
SimpleLdapAuthenticator.login_format = "uid=%s,ou=Users,dc=#{DOMAIN},dc=#{TLD}"
|
||||||
|
|
||||||
|
p SimpleLdapAuthenticator.valid?('admin', 'adminpassword')
|
||||||
+23
-23
@@ -17,16 +17,16 @@ $logger = Logger.new($stderr)
|
|||||||
|
|
||||||
class LDAPController
|
class LDAPController
|
||||||
def self.bind(request, version, dn, password, params)
|
def self.bind(request, version, dn, password, params)
|
||||||
$logger.debug "Catchall bind request"
|
$logger.debug 'Catchall bind request'
|
||||||
raise LDAP::ResultError::UnwillingToPerform, "Invalid bind DN"
|
raise LDAP::ResultError::UnwillingToPerform, 'Invalid bind DN'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.bindUser(request, version, dn, password, params)
|
def self.bindUser(request, version, dn, password, params)
|
||||||
if params[:uid].nil? or
|
if params[:uid].nil? or
|
||||||
params[:uid] != 'admin' or
|
params[:uid] != 'admin' or
|
||||||
password != 'adminpassword'
|
password != 'adminpassword'
|
||||||
$logger.warn "Denied access for user #{params[:uid]}: Invalid credentials"
|
$logger.warn "Denied access for user #{params[:uid]}: Invalid credentials"
|
||||||
raise LDAP::ResultError::InvalidCredentials, "Invalid credentials"
|
raise LDAP::ResultError::InvalidCredentials, 'Invalid credentials'
|
||||||
end
|
end
|
||||||
|
|
||||||
$logger.info "Authenticated user #{params[:uid]}"
|
$logger.info "Authenticated user #{params[:uid]}"
|
||||||
@@ -34,28 +34,28 @@ class LDAPController
|
|||||||
|
|
||||||
def self.search(request, baseObject, scope, deref, filter, params)
|
def self.search(request, baseObject, scope, deref, filter, params)
|
||||||
$logger.info "Catchall search request for #{baseObject}"
|
$logger.info "Catchall search request for #{baseObject}"
|
||||||
raise LDAP::ResultError::UnwillingToPerform, "Invalid search DN"
|
raise LDAP::ResultError::UnwillingToPerform, 'Invalid search DN'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.searchUsers(request, baseObject, scope, deref, filter, params)
|
def self.searchUsers(request, baseObject, scope, deref, filter, params)
|
||||||
$logger.info "Search users"
|
$logger.info 'Search users'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
router = LDAP::Server::Router.new($logger) do
|
router = LDAP::Server::Router.new($logger) do
|
||||||
# Different syntax but same thing
|
# Different syntax but same thing
|
||||||
bind nil => "LDAPController#bind"
|
bind nil => 'LDAPController#bind'
|
||||||
route :bind, nil => "LDAPController#bind"
|
route :bind, nil => 'LDAPController#bind'
|
||||||
|
|
||||||
# Bind a route using variables. A hash with the variables will be passed
|
# Bind a route using variables. A hash with the variables will be passed
|
||||||
# to your function as last argument.
|
# to your function as last argument.
|
||||||
bind "uid=:uid,ou=Users,dc=mydomain,dc=com" => "LDAPController#bindUser"
|
bind 'uid=:uid,ou=Users,dc=mydomain,dc=com' => 'LDAPController#bindUser'
|
||||||
|
bind 'uid=:uid,ou=Users,dc=:domain,dc=:tld' => 'LDAPController#bindUser'
|
||||||
|
|
||||||
search nil => "LDAPController#search"
|
search nil => 'LDAPController#search'
|
||||||
search "ou=Users,dc=mydomain,dc=com" => "LDAPController#searchUsers"
|
search 'ou=Users,dc=mydomain,dc=com' => 'LDAPController#searchUsers'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# This is the shared object which carries our actual directory entries.
|
# 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,...]}
|
# It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
|
||||||
|
|
||||||
@@ -65,26 +65,26 @@ directory = {}
|
|||||||
|
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
begin
|
begin
|
||||||
File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
|
File.open('ldapdb.yaml') { |f| directory = YAML.load(f.read) }
|
||||||
rescue Errno::ENOENT
|
rescue Errno::ENOENT
|
||||||
end
|
end
|
||||||
|
|
||||||
at_exit do
|
at_exit do
|
||||||
File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
|
File.open('ldapdb.new', 'w') { |f| f.write(YAML.dump(directory)) }
|
||||||
File.rename("ldapdb.new","ldapdb.yaml")
|
File.rename('ldapdb.new', 'ldapdb.yaml')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Listen for incoming LDAP connections. For each one, create a Connection
|
# Listen for incoming LDAP connections. For each one, create a Connection
|
||||||
# object, which will invoke a HashOperation object for each request.
|
# object, which will invoke a HashOperation object for each request.
|
||||||
|
|
||||||
s = LDAP::Server.new(
|
s = LDAP::Server.new(
|
||||||
:port => 1389,
|
port: 1389,
|
||||||
:nodelay => true,
|
nodelay: true,
|
||||||
:listen => 10,
|
listen: 10,
|
||||||
# :ssl_key_file => "key.pem",
|
# :ssl_key_file => "key.pem",
|
||||||
# :ssl_cert_file => "cert.pem",
|
# :ssl_cert_file => "cert.pem",
|
||||||
# :ssl_on_connect => true,
|
# :ssl_on_connect => true,
|
||||||
:router => router
|
router: router
|
||||||
)
|
)
|
||||||
s.run_tcpserver
|
s.run_tcpserver
|
||||||
s.join
|
s.join
|
||||||
|
|||||||
+198
-205
@@ -5,216 +5,209 @@ require 'ldap/server/request'
|
|||||||
require 'ldap/server/filter'
|
require 'ldap/server/filter'
|
||||||
|
|
||||||
module LDAP
|
module LDAP
|
||||||
class Server
|
class Server
|
||||||
class Router
|
class Router
|
||||||
@logger
|
# Scope
|
||||||
@routes
|
BaseObject = 0
|
||||||
|
SingleLevel = 1
|
||||||
|
WholeSubtree = 2
|
||||||
|
|
||||||
# Scope
|
# DerefAliases
|
||||||
BaseObject = 0
|
NeverDerefAliases = 0
|
||||||
SingleLevel = 1
|
DerefInSearching = 1
|
||||||
WholeSubtree = 2
|
DerefFindingBaseObj = 2
|
||||||
|
DerefAlways = 3
|
||||||
|
|
||||||
# DerefAliases
|
def initialize(logger, &block)
|
||||||
NeverDerefAliases = 0
|
@logger = logger
|
||||||
DerefInSearching = 1
|
|
||||||
DerefFindingBaseObj = 2
|
|
||||||
DerefAlways = 3
|
|
||||||
|
|
||||||
def initialize(logger, &block)
|
@routes = {}
|
||||||
@logger = logger
|
@routes = Trie.new do |trie|
|
||||||
|
# Add an artificial LDAP component
|
||||||
|
trie << 'op=bind'
|
||||||
|
trie << 'op=search'
|
||||||
|
end
|
||||||
|
|
||||||
@routes = Hash.new
|
instance_eval(&block)
|
||||||
@routes = Trie.new do |trie|
|
|
||||||
# Add an artificial LDAP component
|
|
||||||
trie << "op=bind"
|
|
||||||
trie << "op=search"
|
|
||||||
end
|
|
||||||
|
|
||||||
self.instance_eval(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def log_exception(e, level = :error)
|
|
||||||
@logger.send level, e.message
|
|
||||||
e.backtrace.each { |line| @logger.send level, "\tfrom#{line}" } if e.backtrace
|
|
||||||
end
|
|
||||||
|
|
||||||
######################
|
|
||||||
### Initialization ###
|
|
||||||
######################
|
|
||||||
|
|
||||||
def route(operation, hash)
|
|
||||||
hash.each do |key, value|
|
|
||||||
if key.nil?
|
|
||||||
@routes.insert "op=#{operation.to_s}", value
|
|
||||||
@logger.debug "map operation #{operation.to_s} all routes to #{value}"
|
|
||||||
else
|
|
||||||
@routes.insert "#{key},op=#{operation.to_s}", value
|
|
||||||
@logger.debug "map #{operation.to_s} #{key} to #{value}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_exception(e, level = :error)
|
||||||
|
@logger.send level, e.message
|
||||||
|
e.backtrace.each { |line| @logger.send level, "\tfrom#{line}" } if e.backtrace
|
||||||
|
end
|
||||||
|
|
||||||
|
######################
|
||||||
|
### Initialization ###
|
||||||
|
######################
|
||||||
|
|
||||||
|
def route(operation, hash)
|
||||||
|
hash.each do |key, value|
|
||||||
|
if key.nil?
|
||||||
|
@routes.insert "op=#{operation}", value
|
||||||
|
@logger.debug "map operation #{operation} all routes to #{value}"
|
||||||
|
else
|
||||||
|
@routes.insert "#{key},op=#{operation}", value
|
||||||
|
@logger.debug "map #{operation} #{key} to #{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(name, *args, &block)
|
||||||
|
if %i[bind search add modify modifydn del compare].include? name
|
||||||
|
send :route, name, *args
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
####################################################
|
||||||
|
### Methods to parse and route each request type ###
|
||||||
|
####################################################
|
||||||
|
def parse_route(dn, method)
|
||||||
|
route, action = @routes.match("#{dn},op=#{method}")
|
||||||
|
if !route or route.empty?
|
||||||
|
@logger.warn "No route defined for \'#{route}\'"
|
||||||
|
raise LDAP::ResultError::UnwillingToPerform
|
||||||
|
end
|
||||||
|
if action.nil?
|
||||||
|
@logger.error "No action defined for route \'#{route}\'"
|
||||||
|
raise LDAP::ResultError::UnwillingToPerform
|
||||||
|
end
|
||||||
|
|
||||||
|
class_name = action.split('#').first
|
||||||
|
method_name = action.split('#').last
|
||||||
|
|
||||||
|
params = LDAP::Server::DN.new("#{dn},op=#{method}").parse(route)
|
||||||
|
|
||||||
|
[class_name, method_name, params]
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_bind(connection, messageId, protocolOp, _controls) # :nodoc:
|
||||||
|
request = Request.new(connection, messageId)
|
||||||
|
version = protocolOp.value[0].value
|
||||||
|
dn = protocolOp.value[1].value
|
||||||
|
dn = nil if dn.empty?
|
||||||
|
authentication = protocolOp.value[2]
|
||||||
|
|
||||||
|
@logger.debug "subject:#{connection.binddn} predicate:bind object:#{dn}"
|
||||||
|
|
||||||
|
# Find a route in the routing tree
|
||||||
|
class_name, method_name, params = parse_route(dn, :bind)
|
||||||
|
|
||||||
|
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
|
||||||
|
when 0
|
||||||
|
Object.const_get(class_name).send method_name, request, version, dn, authentication.value, params
|
||||||
|
when 3
|
||||||
|
authentication.value[0].value
|
||||||
|
authentication.value[1].value
|
||||||
|
# sasl_bind(version, dn, mechanism, credentials)
|
||||||
|
# FIXME: needs to exchange further BindRequests
|
||||||
|
# route_sasl_bind(request, version, dn, mechanism, credentials)
|
||||||
|
raise LDAP::ResultError::AuthMethodNotSupported
|
||||||
|
else
|
||||||
|
raise LDAP::ResultError::ProtocolError, 'BindRequest bad AuthenticationChoice'
|
||||||
|
end
|
||||||
|
request.send_BindResponse(0)
|
||||||
|
[dn, version]
|
||||||
|
rescue NoMethodError => e
|
||||||
|
log_exception e
|
||||||
|
request.send_BindResponse(LDAP::ResultError::OperationsError.new.to_i, errorMessage: e.message)
|
||||||
|
[nil, version]
|
||||||
|
rescue LDAP::ResultError => e
|
||||||
|
log_exception e
|
||||||
|
request.send_BindResponse(e.to_i, errorMessage: e.message)
|
||||||
|
[nil, version]
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_search(connection, messageId, protocolOp, _controls) # :nodoc:
|
||||||
|
request = Request.new(connection, messageId)
|
||||||
|
server = connection.opt[:server]
|
||||||
|
schema = connection.opt[:schema]
|
||||||
|
baseObject = protocolOp.value[0].value
|
||||||
|
scope = protocolOp.value[1].value
|
||||||
|
deref = protocolOp.value[2].value
|
||||||
|
client_sizelimit = protocolOp.value[3].value
|
||||||
|
client_timelimit = protocolOp.value[4].value.to_i
|
||||||
|
request.typesOnly = protocolOp.value[5].value
|
||||||
|
filter = LDAP::Server::Filter.parse(protocolOp.value[6], schema)
|
||||||
|
request.attributes = protocolOp.value[7].value.collect { |x| x.value }
|
||||||
|
|
||||||
|
sizelimit = request.server_sizelimit
|
||||||
|
sizelimit = client_sizelimit if client_sizelimit > 0 and
|
||||||
|
(sizelimit.nil? or client_sizelimit < sizelimit)
|
||||||
|
request.sizelimit = sizelimit
|
||||||
|
|
||||||
|
if baseObject.empty? and scope == BaseObject
|
||||||
|
request.send_SearchResultEntry('', server.root_dse) if
|
||||||
|
server.root_dse and LDAP::Server::Filter.run(filter, server.root_dse)
|
||||||
|
request.send_SearchResultDone(0)
|
||||||
|
return
|
||||||
|
elsif schema and baseObject == schema.subschema_dn
|
||||||
|
request.send_SearchResultEntry(baseObject, schema.subschema_subentry) if
|
||||||
|
schema and schema.subschema_subentry and
|
||||||
|
LDAP::Server::Filter.run(filter, schema.subschema_subentry)
|
||||||
|
request.send_SearchResultDone(0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
t = request.server_timelimit || 10
|
||||||
|
t = client_timelimit if client_timelimit > 0 and client_timelimit < t
|
||||||
|
|
||||||
|
@logger.debug "subject:#{connection.binddn} predicate:search object:#{baseObject}"
|
||||||
|
|
||||||
|
# Find a route in the routing tree
|
||||||
|
class_name, method_name, params = parse_route(baseObject, :search)
|
||||||
|
|
||||||
|
Timeout.timeout(t, LDAP::ResultError::TimeLimitExceeded) do
|
||||||
|
Object.const_get(class_name).send method_name, request, baseObject, scope, deref, filter, params
|
||||||
|
end
|
||||||
|
request.send_SearchResultDone(0)
|
||||||
|
|
||||||
|
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
|
||||||
|
rescue LDAP::ResultError => e
|
||||||
|
request.send_SearchResultDone(e.to_i, errorMessage: e.message)
|
||||||
|
rescue Abandon
|
||||||
|
# send no response
|
||||||
|
|
||||||
|
# Since this Operation is running in its own thread, we have to
|
||||||
|
# catch all other exceptions. Otherwise, in the event of a programming
|
||||||
|
# error, this thread will silently terminate and the client will wait
|
||||||
|
# forever for a response.
|
||||||
|
rescue Exception => e
|
||||||
|
log_exception e
|
||||||
|
request.send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, errorMessage: e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
### Methods to actually perform the work requested ###
|
||||||
|
### Use the signatures below to write your own handlers ###
|
||||||
|
###########################################################
|
||||||
|
|
||||||
|
# Handle a simple bind request; raise an exception if the bind is
|
||||||
|
# not acceptable, otherwise just return to accept the bind.
|
||||||
|
#
|
||||||
|
# Write your own class method using this signature
|
||||||
|
|
||||||
|
# def simple_bind(request, version, dn, password, params)
|
||||||
|
# if version != 3
|
||||||
|
# raise LDAP::ResultError::ProtocolError, "version 3 only"
|
||||||
|
# end
|
||||||
|
# if dn
|
||||||
|
# raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Handle a search request
|
||||||
|
#
|
||||||
|
# Call request. send_SearchResultEntry for each result found. Raise
|
||||||
|
# an exception if there is a problem. timeLimit, sizeLimit and
|
||||||
|
# typesOnly are taken care of, but you need to perform all
|
||||||
|
# authorisation checks yourself, using @connection.binddn
|
||||||
|
|
||||||
|
# def search(basedn, scope, deref, filter)
|
||||||
|
# debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
|
||||||
|
# raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def method_missing(name, *args, &block)
|
|
||||||
if [:bind, :search, :add, :modify, :modifydn, :del, :compare].include? name
|
|
||||||
send :route, name, *args
|
|
||||||
else
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
####################################################
|
|
||||||
### Methods to parse and route each request type ###
|
|
||||||
####################################################
|
|
||||||
def parse_route(dn, method)
|
|
||||||
route, action = @routes.match("#{dn},op=#{method.to_s}")
|
|
||||||
if not route or route.empty?
|
|
||||||
@logger.warn "No route defined for \'#{route}\'"
|
|
||||||
raise LDAP::ResultError::UnwillingToPerform
|
|
||||||
end
|
|
||||||
if action.nil?
|
|
||||||
@logger.error "No action defined for route \'#{route}\'"
|
|
||||||
raise LDAP::ResultError::UnwillingToPerform
|
|
||||||
end
|
|
||||||
|
|
||||||
class_name = action.split('#').first
|
|
||||||
method_name = action.split('#').last
|
|
||||||
|
|
||||||
params = LDAP::Server::DN.new("#{dn},op=#{method.to_s}").parse(route)
|
|
||||||
|
|
||||||
return class_name, method_name, params
|
|
||||||
end
|
|
||||||
|
|
||||||
def do_bind(connection, messageId, protocolOp, controls) # :nodoc:
|
|
||||||
request = Request.new(connection, messageId)
|
|
||||||
version = protocolOp.value[0].value
|
|
||||||
dn = protocolOp.value[1].value
|
|
||||||
dn = nil if dn.empty?
|
|
||||||
authentication = protocolOp.value[2]
|
|
||||||
|
|
||||||
@logger.debug "subject:#{connection.binddn} predicate:bind object:#{dn}"
|
|
||||||
|
|
||||||
# Find a route in the routing tree
|
|
||||||
class_name, method_name, params = parse_route(dn, :bind)
|
|
||||||
|
|
||||||
case authentication.tag # tag_class == :CONTEXT_SPECIFIC (check why)
|
|
||||||
when 0
|
|
||||||
Object.const_get(class_name).send method_name, request, version, dn, authentication.value, params
|
|
||||||
when 3
|
|
||||||
mechanism = authentication.value[0].value
|
|
||||||
credentials = authentication.value[1].value
|
|
||||||
# sasl_bind(version, dn, mechanism, credentials)
|
|
||||||
# FIXME: needs to exchange further BindRequests
|
|
||||||
# route_sasl_bind(request, version, dn, mechanism, credentials)
|
|
||||||
raise LDAP::ResultError::AuthMethodNotSupported
|
|
||||||
else
|
|
||||||
raise LDAP::ResultError::ProtocolError, "BindRequest bad AuthenticationChoice"
|
|
||||||
end
|
|
||||||
request.send_BindResponse(0)
|
|
||||||
return dn, version
|
|
||||||
rescue NoMethodError => e
|
|
||||||
log_exception e
|
|
||||||
request.send_BindResponse(LDAP::ResultError::OperationsError.new.to_i, :errorMessage => e.message)
|
|
||||||
return nil, version
|
|
||||||
rescue LDAP::ResultError => e
|
|
||||||
log_exception e
|
|
||||||
request.send_BindResponse(e.to_i, :errorMessage => e.message)
|
|
||||||
return nil, version
|
|
||||||
end
|
|
||||||
|
|
||||||
def do_search(connection, messageId, protocolOp, controls) # :nodoc:
|
|
||||||
request = Request.new(connection, messageId)
|
|
||||||
server = connection.opt[:server]
|
|
||||||
schema = connection.opt[:schema]
|
|
||||||
baseObject = protocolOp.value[0].value
|
|
||||||
scope = protocolOp.value[1].value
|
|
||||||
deref = protocolOp.value[2].value
|
|
||||||
client_sizelimit = protocolOp.value[3].value
|
|
||||||
client_timelimit = protocolOp.value[4].value.to_i
|
|
||||||
request.typesOnly = protocolOp.value[5].value
|
|
||||||
filter = LDAP::Server::Filter::parse(protocolOp.value[6], schema)
|
|
||||||
request.attributes = protocolOp.value[7].value.collect {|x| x.value}
|
|
||||||
|
|
||||||
sizelimit = request.server_sizelimit
|
|
||||||
sizelimit = client_sizelimit if client_sizelimit > 0 and
|
|
||||||
(sizelimit.nil? or client_sizelimit < sizelimit)
|
|
||||||
request.sizelimit = sizelimit
|
|
||||||
|
|
||||||
if baseObject.empty? and scope == BaseObject
|
|
||||||
request.send_SearchResultEntry("", server.root_dse) if
|
|
||||||
server.root_dse and LDAP::Server::Filter.run(filter, server.root_dse)
|
|
||||||
request.send_SearchResultDone(0)
|
|
||||||
return
|
|
||||||
elsif schema and baseObject == schema.subschema_dn
|
|
||||||
request.send_SearchResultEntry(baseObject, schema.subschema_subentry) if
|
|
||||||
schema and schema.subschema_subentry and
|
|
||||||
LDAP::Server::Filter.run(filter, schema.subschema_subentry)
|
|
||||||
request.send_SearchResultDone(0)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
t = request.server_timelimit || 10
|
|
||||||
t = client_timelimit if client_timelimit > 0 and client_timelimit < t
|
|
||||||
|
|
||||||
@logger.debug "subject:#{connection.binddn} predicate:search object:#{baseObject}"
|
|
||||||
|
|
||||||
# Find a route in the routing tree
|
|
||||||
class_name, method_name, params = parse_route(baseObject, :search)
|
|
||||||
|
|
||||||
Timeout::timeout(t, LDAP::ResultError::TimeLimitExceeded) do
|
|
||||||
Object.const_get(class_name).send method_name, request, baseObject, scope, deref, filter, params
|
|
||||||
end
|
|
||||||
request.send_SearchResultDone(0)
|
|
||||||
|
|
||||||
# Note that TimeLimitExceeded is a subclass of LDAP::ResultError
|
|
||||||
rescue LDAP::ResultError => e
|
|
||||||
request.send_SearchResultDone(e.to_i, :errorMessage=>e.message)
|
|
||||||
|
|
||||||
rescue Abandon
|
|
||||||
# send no response
|
|
||||||
|
|
||||||
# Since this Operation is running in its own thread, we have to
|
|
||||||
# catch all other exceptions. Otherwise, in the event of a programming
|
|
||||||
# error, this thread will silently terminate and the client will wait
|
|
||||||
# forever for a response.
|
|
||||||
|
|
||||||
rescue Exception => e
|
|
||||||
log_exception e
|
|
||||||
request.send_SearchResultDone(LDAP::ResultError::OperationsError.new.to_i, :errorMessage=>e.message)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
|
||||||
### Methods to actually perform the work requested ###
|
|
||||||
### Use the signatures below to write your own handlers ###
|
|
||||||
###########################################################
|
|
||||||
|
|
||||||
# Handle a simple bind request; raise an exception if the bind is
|
|
||||||
# not acceptable, otherwise just return to accept the bind.
|
|
||||||
#
|
|
||||||
# Write your own class method using this signature
|
|
||||||
|
|
||||||
# def simple_bind(request, version, dn, password, params)
|
|
||||||
# if version != 3
|
|
||||||
# raise LDAP::ResultError::ProtocolError, "version 3 only"
|
|
||||||
# end
|
|
||||||
# if dn
|
|
||||||
# raise LDAP::ResultError::InappropriateAuthentication, "This server only supports anonymous bind"
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Handle a search request
|
|
||||||
#
|
|
||||||
# Call request. send_SearchResultEntry for each result found. Raise
|
|
||||||
# an exception if there is a problem. timeLimit, sizeLimit and
|
|
||||||
# typesOnly are taken care of, but you need to perform all
|
|
||||||
# authorisation checks yourself, using @connection.binddn
|
|
||||||
|
|
||||||
# def search(basedn, scope, deref, filter)
|
|
||||||
# debug "search(#{basedn}, #{scope}, #{deref}, #{filter})"
|
|
||||||
# raise LDAP::ResultError::UnwillingToPerform, "search not implemented"
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
+65
-68
@@ -1,88 +1,85 @@
|
|||||||
require 'ldap/server/result'
|
require 'ldap/server/result'
|
||||||
|
|
||||||
module LDAP
|
module LDAP
|
||||||
class Server
|
class Server
|
||||||
|
class Operation
|
||||||
|
# Return true if connection is not authenticated
|
||||||
|
|
||||||
class Operation
|
def anonymous?
|
||||||
|
@connection.binddn.nil?
|
||||||
|
end
|
||||||
|
|
||||||
# Return true if connection is not authenticated
|
# Split dn string into its component parts, returning
|
||||||
|
# [ {attr=>val}, {attr=>val}, ... ]
|
||||||
|
#
|
||||||
|
# This is pretty horrible legacy stuff from X500; see RFC2253 for the
|
||||||
|
# full gore. It's stupid that the LDAP protocol sends the DN in string
|
||||||
|
# form, rather than in ASN1 form (as it does with search filters, for
|
||||||
|
# example), even though the DN syntax is defined in terms of ASN1!
|
||||||
|
#
|
||||||
|
# Attribute names are downcased, but values are not. For any
|
||||||
|
# case-insensitive attributes it's up to you to downcase them.
|
||||||
|
#
|
||||||
|
# Note that only v2 clients should add extra space around the comma.
|
||||||
|
# This is accepted, and so is semicolon instead of comma, but the
|
||||||
|
# full RFC1779 backwards-compatibility rules (e.g. quoted values)
|
||||||
|
# are not implemented.
|
||||||
|
#
|
||||||
|
# I *think* these functions will work correctly with UTF8-encoded
|
||||||
|
# characters, given that a multibyte UTF8 character does not contain
|
||||||
|
# the bytes 00-7F and therefore we cannot confuse '\', '+' etc
|
||||||
|
|
||||||
def anonymous?
|
def self.split_dn(dn)
|
||||||
@connection.binddn.nil?
|
# convert \\ to \5c, \+ to \2b etc
|
||||||
end
|
p "dn=#{dn}"
|
||||||
|
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format '\\%02x', match[1].ord }
|
||||||
|
|
||||||
# Split dn string into its component parts, returning
|
# Now we know that \\ and \, do not exist, it's safe to split
|
||||||
# [ {attr=>val}, {attr=>val}, ... ]
|
parts = dn.split(/\s*[,;]\s*/)
|
||||||
#
|
|
||||||
# This is pretty horrible legacy stuff from X500; see RFC2253 for the
|
|
||||||
# full gore. It's stupid that the LDAP protocol sends the DN in string
|
|
||||||
# form, rather than in ASN1 form (as it does with search filters, for
|
|
||||||
# example), even though the DN syntax is defined in terms of ASN1!
|
|
||||||
#
|
|
||||||
# Attribute names are downcased, but values are not. For any
|
|
||||||
# case-insensitive attributes it's up to you to downcase them.
|
|
||||||
#
|
|
||||||
# Note that only v2 clients should add extra space around the comma.
|
|
||||||
# This is accepted, and so is semicolon instead of comma, but the
|
|
||||||
# full RFC1779 backwards-compatibility rules (e.g. quoted values)
|
|
||||||
# are not implemented.
|
|
||||||
#
|
|
||||||
# I *think* these functions will work correctly with UTF8-encoded
|
|
||||||
# characters, given that a multibyte UTF8 character does not contain
|
|
||||||
# the bytes 00-7F and therefore we cannot confuse '\', '+' etc
|
|
||||||
|
|
||||||
def self.split_dn(dn)
|
parts.collect do |part|
|
||||||
# convert \\ to \5c, \+ to \2b etc
|
res = {}
|
||||||
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format "\\%02x", match[1].ord }
|
|
||||||
|
|
||||||
# Now we know that \\ and \, do not exist, it's safe to split
|
# Split each part into attr=val+attr=val
|
||||||
parts = dn.split(/\s*[,;]\s*/)
|
avs = part.split(/\+/)
|
||||||
|
|
||||||
parts.collect do |part|
|
avs.each do |av|
|
||||||
res = {}
|
# These should all be of form attr=value
|
||||||
|
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}" unless av =~ /^([^=]+)=(.*)$/
|
||||||
|
|
||||||
# Split each part into attr=val+attr=val
|
attr = ::Regexp.last_match(1).downcase
|
||||||
avs = part.split(/\+/)
|
val = ::Regexp.last_match(2)
|
||||||
|
# Now we can decode those bits
|
||||||
avs.each do |av|
|
attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { ::Regexp.last_match(1).hex.chr }
|
||||||
# These should all be of form attr=value
|
val.gsub!(/\\([a-f0-9][a-f0-9])/i) { ::Regexp.last_match(1).hex.chr }
|
||||||
unless av =~ /^([^=]+)=(.*)$/
|
res[attr] = val
|
||||||
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}"
|
|
||||||
end
|
end
|
||||||
attr, val = $1.downcase, $2
|
res
|
||||||
# Now we can decode those bits
|
|
||||||
attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
|
|
||||||
val.gsub!(/\\([a-f0-9][a-f0-9])/i) { $1.hex.chr }
|
|
||||||
res[attr] = val
|
|
||||||
end
|
end
|
||||||
res
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Reverse of split_dn. Join [elements...]
|
# Reverse of split_dn. Join [elements...]
|
||||||
# where each element can be {attr=>val,...} or [[attr,val],...]
|
# where each element can be {attr=>val,...} or [[attr,val],...]
|
||||||
# or just [attr,val]
|
# or just [attr,val]
|
||||||
|
|
||||||
def self.join_dn(elements)
|
def self.join_dn(elements)
|
||||||
dn = ""
|
dn = ''
|
||||||
elements.each do |elem|
|
elements.each do |elem|
|
||||||
av = ""
|
av = ''
|
||||||
elem = [elem] if elem[0].is_a?(String)
|
elem = [elem] if elem[0].is_a?(String)
|
||||||
elem.each do |attr,val|
|
elem.each do |attr, val|
|
||||||
av << "+" unless av == ""
|
av << '+' unless av == ''
|
||||||
|
|
||||||
av << attr << "=" <<
|
av << attr << '=' <<
|
||||||
val.sub(/^([# ])/, '\\\\\\1').
|
val.sub(/^([# ])/, '\\\\\\1')
|
||||||
sub(/( )$/, '\\\\\\1').
|
.sub(/( )$/, '\\\\\\1')
|
||||||
gsub(/([,+"\\<>;])/, '\\\\\\1')
|
.gsub(/([,+"\\<>;])/, '\\\\\\1')
|
||||||
|
end
|
||||||
|
dn << ',' unless dn == ''
|
||||||
|
dn << av
|
||||||
end
|
end
|
||||||
dn << "," unless dn == ""
|
dn
|
||||||
dn << av
|
|
||||||
end
|
end
|
||||||
dn
|
end # class Operation
|
||||||
end
|
end # class Server
|
||||||
|
|
||||||
end # class Operation
|
|
||||||
|
|
||||||
end # class Server
|
|
||||||
end # module LDAP
|
end # module LDAP
|
||||||
|
|||||||
Reference in New Issue
Block a user