3 Commits
79d31f61c4
...
00e4cadda6
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
00e4cadda6 |
server2
|
2 weeks ago |
|
|
a4581ce8d9 |
server
|
2 weeks ago |
|
|
5b07e0ea9f |
client authentication from any domain
|
2 weeks ago |
8 changed files with 584 additions and 302 deletions
-
15README.md
-
27examples/client.rb
-
22examples/ldapdb.yaml
-
46examples/rbslapd4.rb
-
153examples/server1.rb
-
93examples/server2.rb
-
397lib/ldap/server/router.rb
-
133lib/ldap/server/util.rb
@ -0,0 +1,27 @@ |
|||||
|
#!/usr/bin/env ruby |
||||
|
|
||||
|
require 'simple_ldap_authenticator' |
||||
|
|
||||
|
raise 'Error: 2 arguments required => email + password' if ARGV.length != 2 |
||||
|
|
||||
|
email = ARGV.shift |
||||
|
password = ARGV.shift |
||||
|
|
||||
|
# puts "email=#{email}" |
||||
|
user = email.split('@').first |
||||
|
fqdn = email.split('@')[1] |
||||
|
fqdn_items = fqdn.split('.') |
||||
|
tld = fqdn_items.last |
||||
|
domain = fqdn_items[0..-2].join('.') |
||||
|
|
||||
|
# puts "user=#{user}" |
||||
|
# puts "domain=#{domain}" |
||||
|
# puts "tld=#{tld}" |
||||
|
# |
||||
|
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?(user, password) |
||||
@ -1,17 +1,21 @@ |
|||||
--- |
--- |
||||
dc=example,dc=com: |
|
||||
|
dc=zourit,dc=re: |
||||
cn: |
cn: |
||||
- Top object |
- Top object |
||||
cn=fred flintstone,dc=example,dc=com: |
|
||||
|
cn=pvincent,dc=zourit,dc=re: |
||||
cn: |
cn: |
||||
- Fred Flintstone |
|
||||
|
- pvincent |
||||
sn: |
sn: |
||||
- Flintstone |
|
||||
|
- P.Vincent |
||||
mail: |
mail: |
||||
- fred@bedrock.org |
|
||||
- fred.flintstone@bedrock.org |
|
||||
cn=wilma flintstone,dc=example,dc=com: |
|
||||
|
- pvincent@zourit.re |
||||
|
dc=pvincent,dc=re: |
||||
cn: |
cn: |
||||
- Wilma Flintstone |
|
||||
|
- Top object |
||||
|
cn=admin,dc=pvincent,dc=re: |
||||
|
cn: |
||||
|
- admin |
||||
|
sn: |
||||
|
- Vincentdmin |
||||
mail: |
mail: |
||||
- wilma@bedrock.org |
|
||||
|
- admin@pvincent.re |
||||
@ -0,0 +1,153 @@ |
|||||
|
#!/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=<ANY>" |
||||
|
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 |
||||
@ -0,0 +1,93 @@ |
|||||
|
#!/usr/bin/env ruby |
||||
|
|
||||
|
$:.unshift('lib') |
||||
|
$debug = true |
||||
|
|
||||
|
require 'ldap/server' |
||||
|
|
||||
|
$logger = Logger.new($stderr) |
||||
|
# 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 |
||||
|
|
||||
|
# ici, c'est bizarre |
||||
|
# systèmatiquement appelé (SEARCH ou AUTH) |
||||
|
# du coup, je retourne super si pas de dn => SEARCH normal sans authentication |
||||
|
# sinon ça passe aussi !!! |
||||
|
def do_bind(protocolOp, controls) # :nodoc: |
||||
|
dn = protocolOp.value[1].value |
||||
|
dn = nil if dn == '' |
||||
|
return super if dn.nil? |
||||
|
|
||||
|
version = protocolOp.value[0].value |
||||
|
authentication = protocolOp.value[2] |
||||
|
password = authentication.value |
||||
|
$logger.info("AUTHENTICATION ..version=#{version}, dn=#{dn}") |
||||
|
super |
||||
|
end |
||||
|
|
||||
|
# def simple_bind(version, dn, password) |
||||
|
# super.simple_bind(version, dn, password) |
||||
|
# $logger.info("SIMPLE BIND...version=#{version}, dn=#{dn}") |
||||
|
# |
||||
|
# raise LDAP::ResultError::ProtocolError, 'version 3 only' if version != 3 |
||||
|
# raise LDAP::ResultError::InvalidCredentials, 'Invalid credentials' if password.nil? || password == '' |
||||
|
# |
||||
|
# $logger.info('authentication SUCCESS') |
||||
|
# end |
||||
|
# |
||||
|
def search(basedn, scope, deref, filter) |
||||
|
$logger.info("SEARCHING...basedn=#{basedn}, scope=#{scope}, deref=#{deref}, filter=#{filter}") |
||||
|
|
||||
|
basedn = basedn.downcase |
||||
|
result = nil |
||||
|
case scope |
||||
|
when LDAP::Server::BaseObject |
||||
|
# client asked for single object by DN |
||||
|
obj = @hash[basedn] |
||||
|
raise LDAP::ResultError::NoSuchObject unless obj |
||||
|
|
||||
|
result = 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? |
||||
|
|
||||
|
result = send_SearchResultEntry(dn, av) |
||||
|
end |
||||
|
|
||||
|
else |
||||
|
raise LDAP::ResultError::UnwillingToPerform, 'OneLevel not implemented' |
||||
|
|
||||
|
end |
||||
|
$logger.info "result=#{result}" |
||||
|
result |
||||
|
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 = {} |
||||
|
require 'yaml' |
||||
|
File.open('examples/ldapdb.yaml') { |f| directory = YAML.load(f.read) } |
||||
|
$logger.info("DIRECTORY=#{directory}") |
||||
|
|
||||
|
# 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, |
||||
|
operation_class: HashOperation, |
||||
|
operation_args: [directory] |
||||
|
) |
||||
|
|
||||
|
$logger.info('server2 RUNNING...') |
||||
|
s.run_tcpserver |
||||
|
s.join |
||||
@ -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 |
|
||||
|
|
||||
# Return true if connection is not authenticated |
|
||||
|
def anonymous? |
||||
|
@connection.binddn.nil? |
||||
|
end |
||||
|
|
||||
def anonymous? |
|
||||
@connection.binddn.nil? |
|
||||
end |
|
||||
|
# 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 |
||||
|
|
||||
# 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 self.split_dn(dn) |
||||
|
# convert \\ to \5c, \+ to \2b etc |
||||
|
p "dn=#{dn}" |
||||
|
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format '\\%02x', match[1].ord } |
||||
|
|
||||
def self.split_dn(dn) |
|
||||
# convert \\ to \5c, \+ to \2b etc |
|
||||
dn.gsub!(/\\([ #,+"\\<>;])/) { |match| format "\\%02x", match[1].ord } |
|
||||
|
# Now we know that \\ and \, do not exist, it's safe to split |
||||
|
parts = dn.split(/\s*[,;]\s*/) |
||||
|
|
||||
# Now we know that \\ and \, do not exist, it's safe to split |
|
||||
parts = dn.split(/\s*[,;]\s*/) |
|
||||
|
parts.collect do |part| |
||||
|
res = {} |
||||
|
|
||||
parts.collect do |part| |
|
||||
res = {} |
|
||||
|
# Split each part into attr=val+attr=val |
||||
|
avs = part.split(/\+/) |
||||
|
|
||||
# Split each part into attr=val+attr=val |
|
||||
avs = part.split(/\+/) |
|
||||
|
avs.each do |av| |
||||
|
# These should all be of form attr=value |
||||
|
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}" unless av =~ /^([^=]+)=(.*)$/ |
||||
|
|
||||
avs.each do |av| |
|
||||
# These should all be of form attr=value |
|
||||
unless av =~ /^([^=]+)=(.*)$/ |
|
||||
raise LDAP::ResultError::ProtocolError, "Bad DN component: #{av}" |
|
||||
|
attr = ::Regexp.last_match(1).downcase |
||||
|
val = ::Regexp.last_match(2) |
||||
|
# Now we can decode those bits |
||||
|
attr.gsub!(/\\([a-f0-9][a-f0-9])/i) { ::Regexp.last_match(1).hex.chr } |
||||
|
val.gsub!(/\\([a-f0-9][a-f0-9])/i) { ::Regexp.last_match(1).hex.chr } |
||||
|
res[attr] = val |
||||
end |
end |
||||
attr, val = $1.downcase, $2 |
|
||||
# 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 |
|
||||
|
res |
||||
end |
end |
||||
res |
|
||||
end |
end |
||||
end |
|
||||
|
|
||||
# Reverse of split_dn. Join [elements...] |
|
||||
# where each element can be {attr=>val,...} or [[attr,val],...] |
|
||||
# or just [attr,val] |
|
||||
|
# Reverse of split_dn. Join [elements...] |
||||
|
# where each element can be {attr=>val,...} or [[attr,val],...] |
||||
|
# or just [attr,val] |
||||
|
|
||||
def self.join_dn(elements) |
|
||||
dn = "" |
|
||||
elements.each do |elem| |
|
||||
av = "" |
|
||||
elem = [elem] if elem[0].is_a?(String) |
|
||||
elem.each do |attr,val| |
|
||||
av << "+" unless av == "" |
|
||||
|
def self.join_dn(elements) |
||||
|
dn = '' |
||||
|
elements.each do |elem| |
||||
|
av = '' |
||||
|
elem = [elem] if elem[0].is_a?(String) |
||||
|
elem.each do |attr, val| |
||||
|
av << '+' unless av == '' |
||||
|
|
||||
av << attr << "=" << |
|
||||
val.sub(/^([# ])/, '\\\\\\1'). |
|
||||
sub(/( )$/, '\\\\\\1'). |
|
||||
gsub(/([,+"\\<>;])/, '\\\\\\1') |
|
||||
|
av << attr << '=' << |
||||
|
val.sub(/^([# ])/, '\\\\\\1') |
||||
|
.sub(/( )$/, '\\\\\\1') |
||||
|
.gsub(/([,+"\\<>;])/, '\\\\\\1') |
||||
|
end |
||||
|
dn << ',' unless dn == '' |
||||
|
dn << av |
||||
end |
end |
||||
dn << "," unless dn == "" |
|
||||
dn << av |
|
||||
|
dn |
||||
end |
end |
||||
dn |
|
||||
end |
|
||||
|
|
||||
end # class Operation |
|
||||
|
|
||||
end # class Server |
|
||||
|
end # class Operation |
||||
|
end # class Server |
||||
end # module LDAP |
end # module LDAP |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue