ldaptic 0.2.0
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/LICENSE +20 -0
- data/README.rdoc +104 -0
- data/Rakefile +41 -0
- data/lib/ldaptic.rb +151 -0
- data/lib/ldaptic/active_model.rb +37 -0
- data/lib/ldaptic/adapters.rb +90 -0
- data/lib/ldaptic/adapters/abstract_adapter.rb +123 -0
- data/lib/ldaptic/adapters/active_directory_adapter.rb +78 -0
- data/lib/ldaptic/adapters/active_directory_ext.rb +12 -0
- data/lib/ldaptic/adapters/ldap_conn_adapter.rb +262 -0
- data/lib/ldaptic/adapters/net_ldap_adapter.rb +173 -0
- data/lib/ldaptic/adapters/net_ldap_ext.rb +24 -0
- data/lib/ldaptic/attribute_set.rb +283 -0
- data/lib/ldaptic/dn.rb +365 -0
- data/lib/ldaptic/entry.rb +646 -0
- data/lib/ldaptic/error_set.rb +34 -0
- data/lib/ldaptic/errors.rb +136 -0
- data/lib/ldaptic/escape.rb +110 -0
- data/lib/ldaptic/filter.rb +282 -0
- data/lib/ldaptic/methods.rb +387 -0
- data/lib/ldaptic/railtie.rb +9 -0
- data/lib/ldaptic/schema.rb +246 -0
- data/lib/ldaptic/syntaxes.rb +319 -0
- data/test/core.schema +582 -0
- data/test/ldaptic_active_model_test.rb +40 -0
- data/test/ldaptic_adapters_test.rb +35 -0
- data/test/ldaptic_attribute_set_test.rb +57 -0
- data/test/ldaptic_dn_test.rb +110 -0
- data/test/ldaptic_entry_test.rb +22 -0
- data/test/ldaptic_errors_test.rb +23 -0
- data/test/ldaptic_escape_test.rb +47 -0
- data/test/ldaptic_filter_test.rb +53 -0
- data/test/ldaptic_hierarchy_test.rb +90 -0
- data/test/ldaptic_schema_test.rb +44 -0
- data/test/ldaptic_syntaxes_test.rb +66 -0
- data/test/mock_adapter.rb +47 -0
- data/test/rbslapd1.rb +111 -0
- data/test/rbslapd4.rb +172 -0
- data/test/test_helper.rb +2 -0
- metadata +146 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require 'ldaptic/adapters/abstract_adapter'
|
|
2
|
+
|
|
3
|
+
module Ldaptic
|
|
4
|
+
module Adapters
|
|
5
|
+
class NetLDAPAdapter < AbstractAdapter
|
|
6
|
+
|
|
7
|
+
register_as(:net_ldap)
|
|
8
|
+
|
|
9
|
+
def initialize(options)
|
|
10
|
+
require 'net/ldap'
|
|
11
|
+
require 'ldaptic/adapters/net_ldap_ext'
|
|
12
|
+
if defined?(::Net::LDAP) && options.kind_of?(::Net::LDAP)
|
|
13
|
+
options = {:adapter => :net_ldap, :connection => option}
|
|
14
|
+
else
|
|
15
|
+
options = (options || {}).dup
|
|
16
|
+
end
|
|
17
|
+
if connection = options[:connection]
|
|
18
|
+
auth = connection.instance_variable_get(:@auth) || {}
|
|
19
|
+
encryption = connection.instance_variable_get(:@encryption)
|
|
20
|
+
options = {
|
|
21
|
+
:adapter => :net_ldap,
|
|
22
|
+
:host => connection.host,
|
|
23
|
+
:port => connection.port,
|
|
24
|
+
:base => connection.base == "dc=com" ? nil : connection.base,
|
|
25
|
+
:username => auth[:username],
|
|
26
|
+
:password => auth[:password]
|
|
27
|
+
}.merge(options)
|
|
28
|
+
if encryption
|
|
29
|
+
options[:encryption] ||= encryption
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
if options[:username]
|
|
33
|
+
auth = {:method => :simple, :username => options[:username], :password => options[:password]}
|
|
34
|
+
else
|
|
35
|
+
auth = {:method => :anonymous}
|
|
36
|
+
end
|
|
37
|
+
options[:connection] ||= ::Net::LDAP.new(
|
|
38
|
+
:host => options[:host],
|
|
39
|
+
:port => options[:port],
|
|
40
|
+
:encryption => options[:encryption],
|
|
41
|
+
:auth => auth
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
@connection = options.delete(:connection)
|
|
45
|
+
@logger = options.delete(:logger)
|
|
46
|
+
super(options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :connection
|
|
50
|
+
|
|
51
|
+
def add(dn, attributes)
|
|
52
|
+
connection.add(:dn => dn, :attributes => attributes)
|
|
53
|
+
handle_errors
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def modify(dn, attributes)
|
|
57
|
+
if attributes.kind_of?(Hash)
|
|
58
|
+
attributes = attributes.map {|k, v| [:replace, k, v]}
|
|
59
|
+
end
|
|
60
|
+
connection.modify(
|
|
61
|
+
:dn => dn,
|
|
62
|
+
:operations => attributes
|
|
63
|
+
)
|
|
64
|
+
handle_errors
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def delete(dn)
|
|
68
|
+
connection.delete(:dn => dn)
|
|
69
|
+
handle_errors
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rename(dn, new_rdn, delete_old, new_superior = nil)
|
|
73
|
+
connection.rename(:olddn => dn, :newrdn => new_rdn, :delete_attributes => delete_old, :newsuperior => new_superior)
|
|
74
|
+
handle_errors
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
DEFAULT_CAPITALIZATIONS = %w[
|
|
78
|
+
objectClass
|
|
79
|
+
|
|
80
|
+
objectClasses
|
|
81
|
+
attributeTypes
|
|
82
|
+
matchingRules
|
|
83
|
+
matchingRuleUse
|
|
84
|
+
dITStructureRules
|
|
85
|
+
dITContentRules
|
|
86
|
+
nameForms
|
|
87
|
+
ldapSyntaxes
|
|
88
|
+
|
|
89
|
+
configurationNamingContext
|
|
90
|
+
currentTime
|
|
91
|
+
defaultNamingContext
|
|
92
|
+
dn
|
|
93
|
+
dnsHostName
|
|
94
|
+
domainControllerFunctionality
|
|
95
|
+
domainFunctionality
|
|
96
|
+
dsServiceName
|
|
97
|
+
forestFunctionality
|
|
98
|
+
highestCommittedUSN
|
|
99
|
+
isGlobalCatalogReady
|
|
100
|
+
isSynchronized
|
|
101
|
+
ldapServiceName
|
|
102
|
+
namingContexts
|
|
103
|
+
rootDomainNamingContext
|
|
104
|
+
schemaNamingContext
|
|
105
|
+
serverName
|
|
106
|
+
subschemaSubentry
|
|
107
|
+
supportedCapabilities
|
|
108
|
+
supportedControl
|
|
109
|
+
supportedLDAPPolicies
|
|
110
|
+
supportedLDAPVersion
|
|
111
|
+
supportedSASLMechanisms
|
|
112
|
+
].inject({}) { |h, k| h[k.downcase] = k; h }
|
|
113
|
+
|
|
114
|
+
def search(options = {}, &block)
|
|
115
|
+
options = options.merge(:return_result => false)
|
|
116
|
+
connection.search(options) do |entry|
|
|
117
|
+
hash = {}
|
|
118
|
+
entry.each do |attr, val|
|
|
119
|
+
attr = recapitalize(attr)
|
|
120
|
+
hash[attr] = val
|
|
121
|
+
end
|
|
122
|
+
block.call(hash)
|
|
123
|
+
end
|
|
124
|
+
handle_errors
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Convenience method which returns true if the credentials are valid, and
|
|
128
|
+
# false otherwise. The credentials are discarded afterwards.
|
|
129
|
+
def authenticate(dn, password)
|
|
130
|
+
conn = Net::LDAP.new(
|
|
131
|
+
:host => @options[:host],
|
|
132
|
+
:port => @options[:port],
|
|
133
|
+
:encryption => @options[:encryption],
|
|
134
|
+
:auth => {:method => :simple, :username => dn, :password => password}
|
|
135
|
+
)
|
|
136
|
+
conn.bind
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def default_base_dn
|
|
140
|
+
@options[:base] || server_default_base_dn
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def inspect
|
|
144
|
+
"#<#{self.class} #{@connection.inspect}>"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
def recapitalize(attribute)
|
|
149
|
+
attribute = attribute.to_s
|
|
150
|
+
@cached_capitalizations ||= DEFAULT_CAPITALIZATIONS
|
|
151
|
+
caps = @cached_capitalizations[attribute] ||=
|
|
152
|
+
attribute_types.keys.detect do |x|
|
|
153
|
+
x.downcase == attribute.downcase
|
|
154
|
+
end
|
|
155
|
+
if caps
|
|
156
|
+
caps
|
|
157
|
+
else
|
|
158
|
+
logger.warn "Original capitalization for #{attribute} unknown"
|
|
159
|
+
@cached_capitalizations[attribute] = attribute
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def handle_errors
|
|
164
|
+
result = yield if block_given?
|
|
165
|
+
err = @connection.get_operation_result
|
|
166
|
+
Ldaptic::Errors.raise_unless_zero(err.code, err.message)
|
|
167
|
+
result
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'net/ldap'
|
|
2
|
+
module Net # :nodoc:
|
|
3
|
+
class LDAP # :nodoc:
|
|
4
|
+
|
|
5
|
+
class Connection # :nodoc:
|
|
6
|
+
# Monkey-patched in support for new superior.
|
|
7
|
+
def rename args
|
|
8
|
+
old_dn = args[:olddn] or raise "Unable to rename empty DN"
|
|
9
|
+
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
|
|
10
|
+
new_superior = args[:newsuperior]
|
|
11
|
+
delete_attrs = args[:delete_attributes] ? true : false
|
|
12
|
+
|
|
13
|
+
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
|
|
14
|
+
request << new_superior.to_ber(128) if new_superior
|
|
15
|
+
request = request.to_ber_appsequence(12)
|
|
16
|
+
pkt = [next_msgid.to_ber, request].to_ber_sequence
|
|
17
|
+
@conn.write pkt
|
|
18
|
+
|
|
19
|
+
(be = @conn.read_ber(AsnSyntax)) && (pdu = Net::LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" )
|
|
20
|
+
pdu.result_code
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
require 'ldaptic/escape'
|
|
2
|
+
|
|
3
|
+
module Ldaptic
|
|
4
|
+
# AttributeSet, like the name suggests, represents a set of attributes. Most
|
|
5
|
+
# operations are delegated to an array, so the usual array methods should
|
|
6
|
+
# work transparently.
|
|
7
|
+
class AttributeSet
|
|
8
|
+
|
|
9
|
+
attr_reader :entry, :name, :type, :syntax
|
|
10
|
+
|
|
11
|
+
# The original attributes before type conversion. Mutating the result
|
|
12
|
+
# mutates the original attributes.
|
|
13
|
+
def before_type_cast
|
|
14
|
+
@target
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_a
|
|
18
|
+
typecast(@target)
|
|
19
|
+
end
|
|
20
|
+
alias to_ary to_a
|
|
21
|
+
|
|
22
|
+
include Enumerable
|
|
23
|
+
def each(&block)
|
|
24
|
+
to_a.each(&block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(entry, name, target)
|
|
28
|
+
@entry = entry
|
|
29
|
+
@name = Ldaptic.encode(name)
|
|
30
|
+
@type = @entry.namespace.attribute_type(@name)
|
|
31
|
+
@syntax = @entry.namespace.attribute_syntax(@name)
|
|
32
|
+
@target = target
|
|
33
|
+
if @type.nil?
|
|
34
|
+
@entry.logger "Unknown type for attribute #@name"
|
|
35
|
+
elsif @syntax.nil?
|
|
36
|
+
@entry.logger "Unknown syntax #{@type.syntax_oid} for attribute #{@name}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def errors
|
|
41
|
+
return ['is forbidden'] if forbidden? && !empty?
|
|
42
|
+
errors = []
|
|
43
|
+
if single_value? && size > 1
|
|
44
|
+
errors << "does not accept multiple values"
|
|
45
|
+
elsif mandatory? && empty?
|
|
46
|
+
errors << "is mandatory"
|
|
47
|
+
end
|
|
48
|
+
if syntax_object
|
|
49
|
+
errors += @target.map { |v| syntax_object.error(v) }.compact
|
|
50
|
+
end
|
|
51
|
+
errors
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Delegates to an array.
|
|
55
|
+
def method_missing(method, *args, &block)
|
|
56
|
+
to_a.send(method, *args, &block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ===(object)
|
|
60
|
+
to_a === object
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def eql?(object)
|
|
64
|
+
to_a.eql?(object)
|
|
65
|
+
end
|
|
66
|
+
alias == eql?
|
|
67
|
+
|
|
68
|
+
def respond_to?(method, *args) #:nodoc:
|
|
69
|
+
super || @target.respond_to?(method, *args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def size
|
|
73
|
+
@target.size
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def empty?
|
|
77
|
+
@target.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Adds the given attributes, discarding duplicates. Currently, a duplicate
|
|
81
|
+
# is determined by == (case sensitive) rather than by the server (typically
|
|
82
|
+
# case insensitive). All arrays are flattened.
|
|
83
|
+
def add(*attributes)
|
|
84
|
+
dest = @target.dup
|
|
85
|
+
safe_array(attributes).each do |attribute|
|
|
86
|
+
dest.push(attribute) unless include?(attribute)
|
|
87
|
+
end
|
|
88
|
+
replace(dest)
|
|
89
|
+
end
|
|
90
|
+
alias << add
|
|
91
|
+
alias concat add
|
|
92
|
+
alias push add
|
|
93
|
+
|
|
94
|
+
# Add the desired attributes to the LDAP server immediately.
|
|
95
|
+
def add!(*attributes)
|
|
96
|
+
@entry.add!(@name, safe_array(attributes))
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Does a complete replacement of the attributes. Multiple attributes can
|
|
101
|
+
# be given as either multiple arguments or as an array.
|
|
102
|
+
def replace(*attributes)
|
|
103
|
+
attributes = safe_array(attributes)
|
|
104
|
+
user_modification_guard
|
|
105
|
+
@target.replace(attributes)
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Replace the entire attribute at the LDAP server immediately.
|
|
110
|
+
def replace!(*attributes)
|
|
111
|
+
@entry.replace!(@name, safe_array(attributes))
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def clear
|
|
116
|
+
replace([])
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Remove the given attributes given, functioning more or less like
|
|
121
|
+
# Array#delete, except accepting multiple arguments.
|
|
122
|
+
#
|
|
123
|
+
# Two passes are made to find each element, one case sensitive and one
|
|
124
|
+
# ignoring case, before giving up.
|
|
125
|
+
def delete(*attributes, &block)
|
|
126
|
+
return clear if attributes.flatten.empty?
|
|
127
|
+
dest = @target.dup
|
|
128
|
+
ret = []
|
|
129
|
+
safe_array(attributes).each do |attribute|
|
|
130
|
+
ret << dest.delete(attribute) do
|
|
131
|
+
match = dest.detect {|x| x.downcase == attribute.downcase}
|
|
132
|
+
if match
|
|
133
|
+
dest.delete(match)
|
|
134
|
+
else
|
|
135
|
+
yield(attribute) if block_given?
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
replace(dest)
|
|
140
|
+
if attributes.size == 1 && !attributes.first.kind_of?(Array)
|
|
141
|
+
typecast ret.first
|
|
142
|
+
else
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
alias subtract delete
|
|
147
|
+
|
|
148
|
+
# Delete the desired values from the attribute at the LDAP server.
|
|
149
|
+
# If no values are given, the entire attribute is removed.
|
|
150
|
+
def delete!(*attributes)
|
|
151
|
+
@entry.delete!(@name, safe_array(attributes))
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def collect!(&block)
|
|
156
|
+
replace(to_a.collect(&block))
|
|
157
|
+
end
|
|
158
|
+
alias map! collect!
|
|
159
|
+
|
|
160
|
+
def insert(index, *objects)
|
|
161
|
+
user_modification_guard
|
|
162
|
+
@target.insert(index, *safe_array(objects))
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def unshift(*values)
|
|
167
|
+
insert(0, *values)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def reject!(&block)
|
|
171
|
+
user_modification_guard
|
|
172
|
+
@target.reject! do |value|
|
|
173
|
+
yield(typecast(value))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def delete_if(&block)
|
|
178
|
+
reject!(&block)
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
%w(delete_at pop shift slice!).each do |method|
|
|
183
|
+
class_eval(<<-EOS, __FILE__, __LINE__.succ)
|
|
184
|
+
def #{method}(*args, &block)
|
|
185
|
+
user_modification_guard
|
|
186
|
+
typecast(@target.#{method}(*args, &block))
|
|
187
|
+
end
|
|
188
|
+
EOS
|
|
189
|
+
end
|
|
190
|
+
alias []= slice!
|
|
191
|
+
|
|
192
|
+
%w(reverse! shuffle! sort! uniq!).each do |method|
|
|
193
|
+
class_eval(<<-EOS, __FILE__, __LINE__.succ)
|
|
194
|
+
def #{method}(*args)
|
|
195
|
+
Ldaptic::Errors.raise(NotImplementedError.new)
|
|
196
|
+
end
|
|
197
|
+
EOS
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns +true+ if the attribute is marked neither MUST nor MAY in the
|
|
201
|
+
# object class.
|
|
202
|
+
def forbidden?
|
|
203
|
+
!(@entry.must + @entry.may).include?(@name)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns +true+ if the attribute is marked MUST in the object class.
|
|
207
|
+
def mandatory?
|
|
208
|
+
@entry.must.include?(@name)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns +true+ if the attribute may not be specified more than once.
|
|
212
|
+
def single_value?
|
|
213
|
+
@type && @type.single_value?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns +true+ for read only attributes.
|
|
217
|
+
def no_user_modification?
|
|
218
|
+
@type && @type.no_user_modification?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# If the attribute is a single value, return it, otherwise, return self.
|
|
222
|
+
def one
|
|
223
|
+
if single_value?
|
|
224
|
+
first
|
|
225
|
+
else
|
|
226
|
+
self
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
attr_reader :type
|
|
231
|
+
|
|
232
|
+
def to_s
|
|
233
|
+
@target.join("\n")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def inspect
|
|
237
|
+
"<#{to_a.inspect}>"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def as_json(*args) #:nodoc:
|
|
241
|
+
to_a.as_json(*args)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def syntax_object
|
|
245
|
+
@syntax && @syntax.object.new(@entry)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Invokes +human_attribute_name+ on the attribute's name.
|
|
249
|
+
def human_name
|
|
250
|
+
@entry.class.human_attribute_name(@name)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def format(value)
|
|
256
|
+
value = @syntax ? syntax_object.format(value) : value
|
|
257
|
+
if no_user_modification? && value.kind_of?(String)
|
|
258
|
+
value.dup.freeze
|
|
259
|
+
else
|
|
260
|
+
value
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def safe_array(attributes)
|
|
265
|
+
Array(attributes).flatten.compact.map {|x| format(x)}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def typecast(value)
|
|
269
|
+
case value
|
|
270
|
+
when Array then value.map {|x| typecast(x)}
|
|
271
|
+
when nil then nil
|
|
272
|
+
else @syntax ? syntax_object.parse(value) : value
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def user_modification_guard
|
|
277
|
+
if no_user_modification?
|
|
278
|
+
Ldaptic::Errors.raise(TypeError.new("read-only attribute #{@name}"))
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
end
|
|
283
|
+
end
|