arldap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in arldap.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,45 @@
1
+ arldap
2
+ =========================
3
+
4
+ This is an implementation of an LDAP server which uses active record as the data source.
5
+ The server is read-only, and can serve information from any AR model that implements the
6
+ #search(string) class method and the #to_ldap_entry instance method.
7
+
8
+ To test, point your addressbook (ie: Outlook, Thunderbird or OS X Address Book) at the server and run a search.
9
+
10
+ Example AR class:
11
+
12
+ class Person < ActiveRecord::Base
13
+ def fullname
14
+ "#{firstname} #{lastname}"
15
+ end
16
+
17
+ def to_ldap_entry
18
+ {
19
+ "objectclass" => ["top", "person", "organizationalPerson", "inetOrgPerson", "mozillaOrgPerson"],
20
+ "uid" => ["tbotter-#{id}"],
21
+ "sn" => [lastname],
22
+ "givenName" => [firstname],
23
+ "cn" => [fullname],
24
+ "title" => [title],
25
+ "o" => [company],
26
+ "mail" => [email],
27
+ "telephonenumber" => [work_phone],
28
+ "homephone" => [home_phone],
29
+ "fax" => [fax],
30
+ "mobile" => [mobile],
31
+ "street" => [address],
32
+ "l" => [city],
33
+ "st" => [state],
34
+ "postalcode" => [zip],
35
+ }
36
+ end
37
+
38
+ def self.search(query)
39
+ Person.find(:all,
40
+ :conditions => ["(email LIKE ?) OR (firstname LIKE ?) OR (lastname LIKE ?)",
41
+ "#{query}%", "#{query}%", "#{query}%"])
42
+ end
43
+ end
44
+
45
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "arldap/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "arldap"
7
+ s.version = Arldap::VERSION
8
+ s.authors = ["Jody Alkema", "Shane Davies"]
9
+ s.email = ["jody@alkema.ca", "shane@domain7.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Active Record LDAP}
12
+ s.description = %q{Provide an LDAP search interface to an ActiveRecord model in a Rails 3 project}
13
+
14
+ s.rubyforge_project = "arldap"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # s.add_development_dependency "rspec"
22
+ s.add_runtime_dependency "ruby-ldapserver"
23
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+ require 'arldap/server'
5
+
6
+ case ARGV[0]
7
+ when "start": Server.new(ARGV[1]).start
8
+ when "stop": Server.new(ARGV[1]).stop
9
+ when "restart": Server.new(ARGV[1]).restart
10
+ else puts "Usage: #{File.basename(__FILE__)} {start|stop|restart} [config file]"
11
+ end
@@ -0,0 +1,3 @@
1
+ module Arldap
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env ruby
2
+ require "ruby-ldapserver-0.3/lib/ldap/server"
3
+
4
+ class ActiveRecordOperation < LDAP::Server::Operation
5
+ attr_accessor :schema, :server, :attributes, :connection
6
+
7
+ def initialize(connection, messageID, config, ar_class, logger)
8
+ @config, @ar_class, @logger = config, ar_class, logger
9
+ @logger.debug "Received connection request (#{messageID})."
10
+ @connection = connection
11
+ super(connection, messageID)
12
+ end
13
+
14
+ def simple_bind(version, dn, password)
15
+ @logger.debug "config password"
16
+ @logger.debug @config[:password]
17
+ @logger.debug "config password class"
18
+ @logger.debug @config[:password].class
19
+ @logger.debug "passed password"
20
+ @logger.debug password
21
+ @logger.debug "passed password class"
22
+ @logger.debug password.class
23
+
24
+ @connection.opt[:authentic] = password == @config[:password]
25
+ if @connection.opt[:authentic]
26
+ @logger.info "simple_bind authentic: #{@connection.opt[:authentic]}"
27
+ else
28
+ @logger.info "simple_bind not authentic: #{@connection.opt[:authentic]}"
29
+ end
30
+ end
31
+
32
+ def search(basedn, scope, deref, filter)
33
+ if @connection.opt[:authentic]
34
+ @logger.info "Valid credentials: #{@connection.opt[:authentic]}"
35
+ else
36
+ @logger.info "Invalid credentials"
37
+ raise LDAP::ResultError::InvalidCredentials, "Invalid credentials: #{@connection.opt[:authentic]}"
38
+ end
39
+
40
+ # This is needed to force the ruby ldap server to return our parameters,
41
+ # even though the client didn't explicitly ask for them
42
+ @attributes << "*"
43
+ if basedn != @config[:basedn]
44
+ @logger.info "Denying request with missmatched basedn (wanted \"#{@config[:basedn]}\", but got \"#{basedn}\")"
45
+ raise LDAP::ResultError::UnwillingToPerform, "Bad base DN"
46
+ end
47
+
48
+ if scope == LDAP::Server::BaseObject
49
+ @logger.info "Denying request for BaseObject: #{filter.inspect}"
50
+ raise LDAP::ResultError::UnwillingToPerform, "BaseObject not implemented: #{filter}"
51
+ end
52
+
53
+ query_string = parse_filter(filter)
54
+
55
+ @logger.debug "Running #{@ar_class.name}.ldap_search(\"#{query_string}\")"
56
+
57
+ begin
58
+ @records = @ar_class.ldap_search(query_string)
59
+ rescue Exception => e
60
+ @logger.error "ERROR running #{@ar_class.name}.ldap_search(#{query_string}): #{e}"
61
+ # raise LDAP::ResultError::OperationsError, "Error encountered during processing: #{e}."
62
+ end
63
+
64
+ @logger.info "Returning #{@records.size} records matching \"#{query_string}\"."
65
+
66
+ @records.each do |record|
67
+ begin
68
+ ret = record.to_ldap_entry
69
+ rescue
70
+ @logger.error "ERROR converting AR instance to ldap entry: #{$!}"
71
+ raise LDAP::ResultError::OperationsError, "Error encountered during processing."
72
+ end
73
+
74
+ ret_basedn = "uid=#{ret["uid"]},#{@config[:basedn]}"
75
+ @logger.debug "Sending #{ret_basedn} - #{ret.inspect}"
76
+ send_SearchResultEntry(ret_basedn, ret)
77
+ end
78
+ end
79
+
80
+ def parse_filter(filter)
81
+ # format of mozilla and OS X address book searches are always this:
82
+ # [:or, [:substrings, "mail", nil, nil, "XXX", nil],
83
+ # [:substrings, "cn", nil, nil, "XXX", nil],
84
+ # [:substrings, "givenName", nil, nil, "XXX", nil],
85
+ # [:substrings, "sn", nil, nil, "XXX", nil]]
86
+ # (with the order of the subgroups maybe turned around)
87
+ ##
88
+ # [:or, [:substrings, "givenname", nil, "j", nil],
89
+ # [:substrings, "sn", nil, "j", nil],
90
+ # [:substrings, "mail", nil, "j", nil],
91
+ # [:substrings, "cn", nil, "j", nil]]
92
+
93
+ @logger.info filter
94
+
95
+ unless filter
96
+ @logger.info "Denying complex query (error 1): #{filter.inspect}"
97
+ raise LDAP::ResultError::UnwillingToPerform, "This query is way too complex: #{filter.inspect}"
98
+ end
99
+
100
+ if filter[0] != :or
101
+ @logger.info "Denying complex query (error 1): #{filter.inspect}"
102
+ raise LDAP::ResultError::UnwillingToPerform, "This query is way too complex: #{filter.inspect}"
103
+ end
104
+
105
+ query = filter[1]
106
+
107
+ if !(query.length > 3)
108
+ @logger.info "Denying complex query (error 2): #{filter.inspect}"
109
+ raise LDAP::ResultError::UnwillingToPerform, "This query is way too complex: #{filter.inspect}"
110
+ end
111
+
112
+ if !query[1]
113
+ @logger.info "Refusing to respond to blank query string: #{filter.inspect}."
114
+ raise LDAP::ResultError::UnwillingToPerform, "Refusing to respond to blank query string: #{filter.inspect}"
115
+ end
116
+
117
+ if !(%w{mail cn givenname sn}.include? query[1].downcase)
118
+ @logger.info "Denying complex query (error 3): #{filter.inspect}"
119
+ raise LDAP::ResultError::UnwillingToPerform, "This query is way too complex: #{filter.inspect}"
120
+ end
121
+
122
+ query_string = query[2..-1].compact.first # We're just going to take the first non-nil element as the search string
123
+
124
+ if !query_string
125
+ @logger.info "Refusing to respond to blank query string: #{filter.inspect}."
126
+ raise LDAP::ResultError::UnwillingToPerform, "Refusing to respond to blank query string: #{filter.inspect}"
127
+ end
128
+
129
+ query_string
130
+ end
131
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ class Logger
4
+ attr_accessor :logdev
5
+ end
6
+
7
+ module Daemon
8
+ def daemonize(logger = nil)
9
+ # This causes the grandchild process to be orphaned,
10
+ # so the init process is responsible for cleaning it up.
11
+ Kernel.fork and Kernel.exit
12
+ Process.setsid
13
+ Kernel.fork and Kernel.exit
14
+
15
+ File.umask 0
16
+ Dir.chdir '/'
17
+
18
+ ObjectSpace.each_object(IO) do |io|
19
+ unless (logger and logger.logdev.dev == io)
20
+ io.close rescue nil
21
+ end
22
+ end
23
+
24
+ STDIN.reopen( '/dev/null')
25
+ STDOUT.reopen('/dev/null', 'a')
26
+ STDERR.reopen('/dev/null', 'a')
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'fileutils'
2
+ class PidFile
3
+ attr_reader :file
4
+ def initialize(file)
5
+ @file = file
6
+ end
7
+
8
+ def pid
9
+ File.file?(@file) and IO.read(@file)
10
+ end
11
+
12
+ def remove
13
+ if self.pid
14
+ FileUtils.rm @file
15
+ end
16
+ end
17
+
18
+ def create
19
+ File.open(@file, "w") { |f| f.write($$) }
20
+ end
21
+
22
+ def ensure_empty!(msg = nil)
23
+ if self.pid
24
+ puts msg if msg
25
+ exit 1
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,123 @@
1
+ $:.unshift File.join(File.dirname(__FILE__))
2
+
3
+ %w( yaml
4
+ erb
5
+ etc
6
+ fileutils
7
+ logger
8
+ thread
9
+ ldap
10
+ resolv-replace
11
+ active_record_operation
12
+ pid_file
13
+ daemon
14
+ ).each do |lib|
15
+ require lib
16
+ end
17
+
18
+ class Server
19
+ attr_accessor :config, :logger, :pidfile
20
+ include Daemon
21
+
22
+ @@def_config_file_name = File.expand_path(File.join(File.dirname(__FILE__), "..", "conf", "ldap-server.yml"))
23
+
24
+ def initialize(config_file_name = nil)
25
+ config_file_name ||= @@def_config_file_name
26
+
27
+ @config = YAML.load(ERB.new(File.read(config_file_name)).result)
28
+
29
+ self.logger = Logger.new(@config["log_file"] || File.join(@config["rails_dir"], *%w(log ldap-server.log)))
30
+ self.logger.level = @config["debug"] ? Logger::DEBUG : Logger::INFO
31
+ self.logger.datetime_format = "%H:%M:%S"
32
+ self.logger.info ""
33
+ @config["pid_file"] ||= @config["pid_file"] || File.join(@config["rails_dir"], *%w(log ldap-server.pid))
34
+ @pidfile = PidFile.new(@config["pid_file"])
35
+ end
36
+
37
+ def become_user(username = 'nobody', chroot = false)
38
+ user = Etc::getpwnam(username)
39
+
40
+ Dir.chroot(user.dir) and Dir.chdir('/') if chroot
41
+
42
+ Process::initgroups(username, user.gid)
43
+ Process::Sys::setegid(user.gid)
44
+ Process::Sys::setgid(user.gid)
45
+ Process::Sys::setuid(user.uid)
46
+ end
47
+
48
+ def start
49
+ pidfile.ensure_empty! "ERROR: It looks like I'm already running #{@config['pid_file']}. Not starting."
50
+
51
+ logger.info "Starting LDAP server"
52
+ daemonize(logger)
53
+
54
+ self.logger.info("Cannot load Rails. Exiting.") and exit 5 unless defined? RAILS_ROOT
55
+ @config.symbolize_keys!
56
+
57
+ logger.info "Became daemon with process id: #{$$}"
58
+ begin
59
+ pidfile.create
60
+ rescue Exception => e
61
+ logger.info "Exception caught while creating pidfile: #{e}"
62
+ exit
63
+ end
64
+
65
+ trap("TERM") do
66
+ logger.info("Received TERM signal. Exiting.") if logger
67
+ pidfile.remove if pidfile
68
+ exit
69
+ end
70
+
71
+ begin
72
+ # This is to ensure thread-safety
73
+ # logger.debug "Setting allow_concurrency"
74
+ # ActiveRecord::Base.allow_concurrency = true
75
+ rescue Exception => e
76
+ logger.info "Exception caught: #{e}"
77
+ exit
78
+ end
79
+
80
+ klass = nil
81
+ begin
82
+ klass = @config[:active_record_model].constantize
83
+ logger.info "Access to #{klass.count} #{@config[:active_record_model]} records"
84
+ rescue Exception => e
85
+ logger.info "Exception caught while loading #{@config[:active_record_model]}: #{e}"
86
+ exit
87
+ end
88
+
89
+ s = LDAP::Server.new(
90
+ :port => @config[:port],
91
+ :bindaddr => @config[:bind_address],
92
+ :nodelay => @config[:tcp_nodelay],
93
+ :listen => @config[:prefork_threads],
94
+ :namingContexts => [@config[:basedn]],
95
+ :user => @config[:user],
96
+ :group => @config[:group],
97
+ :operation_class => ActiveRecordOperation,
98
+ :operation_args => [@config, klass, logger]
99
+ )
100
+ s.run_tcpserver
101
+ logger.info "Listening on port #{@config[:port]}."
102
+
103
+ s.join
104
+ end
105
+
106
+ def stop
107
+ if @pidfile.pid
108
+ puts "Sending TERM signal to process #{@pidfile.pid}" if @config[:debug]
109
+ logger.info("Killing server at #{@pidfile.pid}")
110
+ Process.kill("TERM", @pidfile.pid.to_i)
111
+ else
112
+ puts "Can't find pid. Are you sure I'm running?"
113
+ end
114
+ end
115
+
116
+ def restart
117
+ stop
118
+ sleep 5
119
+ start
120
+ end
121
+ end
122
+
123
+
@@ -0,0 +1,3 @@
1
+ module Arldap
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arldap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jody Alkema
9
+ - Shane Davies
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-12-09 00:00:00.000000000Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: ruby-ldapserver
17
+ requirement: &21655100 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *21655100
26
+ description: Provide an LDAP search interface to an ActiveRecord model in a Rails
27
+ 3 project
28
+ email:
29
+ - jody@alkema.ca
30
+ - shane@domain7.com
31
+ executables:
32
+ - arldap
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - .gitignore
37
+ - Gemfile
38
+ - README
39
+ - Rakefile
40
+ - arldap.gemspec
41
+ - bin/arldap
42
+ - lib/arldap.rb
43
+ - lib/arldap/active_record_operation.rb
44
+ - lib/arldap/daemon.rb
45
+ - lib/arldap/pid_file.rb
46
+ - lib/arldap/server.rb
47
+ - lib/arldap/version.rb
48
+ homepage: ''
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project: arldap
68
+ rubygems_version: 1.8.10
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Active Record LDAP
72
+ test_files: []