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.
Files changed (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +104 -0
  3. data/Rakefile +41 -0
  4. data/lib/ldaptic.rb +151 -0
  5. data/lib/ldaptic/active_model.rb +37 -0
  6. data/lib/ldaptic/adapters.rb +90 -0
  7. data/lib/ldaptic/adapters/abstract_adapter.rb +123 -0
  8. data/lib/ldaptic/adapters/active_directory_adapter.rb +78 -0
  9. data/lib/ldaptic/adapters/active_directory_ext.rb +12 -0
  10. data/lib/ldaptic/adapters/ldap_conn_adapter.rb +262 -0
  11. data/lib/ldaptic/adapters/net_ldap_adapter.rb +173 -0
  12. data/lib/ldaptic/adapters/net_ldap_ext.rb +24 -0
  13. data/lib/ldaptic/attribute_set.rb +283 -0
  14. data/lib/ldaptic/dn.rb +365 -0
  15. data/lib/ldaptic/entry.rb +646 -0
  16. data/lib/ldaptic/error_set.rb +34 -0
  17. data/lib/ldaptic/errors.rb +136 -0
  18. data/lib/ldaptic/escape.rb +110 -0
  19. data/lib/ldaptic/filter.rb +282 -0
  20. data/lib/ldaptic/methods.rb +387 -0
  21. data/lib/ldaptic/railtie.rb +9 -0
  22. data/lib/ldaptic/schema.rb +246 -0
  23. data/lib/ldaptic/syntaxes.rb +319 -0
  24. data/test/core.schema +582 -0
  25. data/test/ldaptic_active_model_test.rb +40 -0
  26. data/test/ldaptic_adapters_test.rb +35 -0
  27. data/test/ldaptic_attribute_set_test.rb +57 -0
  28. data/test/ldaptic_dn_test.rb +110 -0
  29. data/test/ldaptic_entry_test.rb +22 -0
  30. data/test/ldaptic_errors_test.rb +23 -0
  31. data/test/ldaptic_escape_test.rb +47 -0
  32. data/test/ldaptic_filter_test.rb +53 -0
  33. data/test/ldaptic_hierarchy_test.rb +90 -0
  34. data/test/ldaptic_schema_test.rb +44 -0
  35. data/test/ldaptic_syntaxes_test.rb +66 -0
  36. data/test/mock_adapter.rb +47 -0
  37. data/test/rbslapd1.rb +111 -0
  38. data/test/rbslapd4.rb +172 -0
  39. data/test/test_helper.rb +2 -0
  40. 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