#!/usr/local/bin/ruby -w $:.unshift('../lib') require 'ldap/server' require '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 -H ldap://127.0.0.1:1389/ -b "dc=example,dc=com" "(uid=brian)" # # ldapsearch -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=)" 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