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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README +45 -0
- data/Rakefile +1 -0
- data/arldap.gemspec +23 -0
- data/bin/arldap +11 -0
- data/lib/arldap.rb +3 -0
- data/lib/arldap/active_record_operation.rb +131 -0
- data/lib/arldap/daemon.rb +28 -0
- data/lib/arldap/pid_file.rb +28 -0
- data/lib/arldap/server.rb +123 -0
- data/lib/arldap/version.rb +3 -0
- metadata +72 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/arldap.gemspec
ADDED
|
@@ -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
|
data/bin/arldap
ADDED
|
@@ -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
|
data/lib/arldap.rb
ADDED
|
@@ -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
|
+
|
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: []
|