arldap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []