ldaptic 0.2.0 → 0.2.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007 Tim Pope
1
+ Copyright (c) Tim Pope
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -29,18 +29,15 @@ a dynamically created module into a namespace of your choosing.
29
29
  )
30
30
  end
31
31
 
32
- The adapter field can usually be omitted as it defaults to :ldap_conn or
33
- :net_ldap, based on which of the above two gems can be found (though you might
34
- want to use the :active_directory adapter, which depends on ruby-ldap,
35
- instead). If the base is omitted, it will use the first naming context on the
36
- server (usually what you want).
32
+ The adapter can usually be omitted as it defaults to :ldap_conn or :net_ldap,
33
+ based on which of the above two gems can be found. If the base is omitted, it
34
+ will use the first naming context on the server (usually what you want).
37
35
 
38
36
  Entries are retrieved using the search method. Named parameters include
39
- :base, :filter, :sort, :limit, :scope, and :attributes. All are optional.
37
+ :base, :scope, :filter, :attributes, :scope, and :limit. All are optional.
40
38
 
41
39
  entries = Example.search(
42
- :filter => {:objectClass => 'inetOrgPerson'}
43
- :sort => :cn,
40
+ :filter => {:objectClass => 'inetOrgPerson'},
44
41
  :limit => 10
45
42
  )
46
43
 
@@ -61,7 +58,7 @@ Predictably, entries have attribute readers and writers.
61
58
  => <["root"]>
62
59
  >> entry[:cn] = "admin"
63
60
 
64
- The returned object is an attribute set and behaves similar to an array. Some
61
+ The returned object is an attribute set and is similar to an array. Some
65
62
  attributes are marked by the server as "single value;" those will return the
66
63
  first element on method access but an attribute set on indexing access, for
67
64
  programmatic convenience.
@@ -80,7 +77,7 @@ The indexing syntax can also be used to create and fetch children.
80
77
  >> users[:cn=>'admin']
81
78
  => #<Example::InetOrgPerson cn=admin,ou=Users,dc=example,dc=com ...>
82
79
 
83
- Entries also implement many of the standard methods you've come to expect in
80
+ Entry also implements many of the standard methods you've come to expect in
84
81
  an Active Record world (save, valid?, errors, to_param, attributes, ...).
85
82
  In fact, it is fully Active Model compliant.
86
83
 
@@ -89,10 +86,6 @@ methods like search), Ldaptic::Entry, and Ldaptic::AttributeSet.
89
86
 
90
87
  == To Do
91
88
 
92
- * Verify everything still works. The tests take care of much of this, but
93
- there are no integration tests for the adapters. In particular, I have no
94
- way to verify the Active Directory adapter still works.
95
-
96
89
  * The test suite (reflecting my fledgling testing abilities from 2007) is more
97
90
  smoke test than BDD. Perhaps switch to RSpec in the quest to rectify this.
98
91
 
@@ -100,5 +93,3 @@ methods like search), Ldaptic::Entry, and Ldaptic::AttributeSet.
100
93
  Record") are in the GitHub issue tracker. Vote for and comment on the ones
101
94
  you would find useful, as most are on hold until someone has a real use
102
95
  case.
103
-
104
- * Pick a better name? Ldaptic has an ambiguous spelling.
@@ -3,6 +3,11 @@ require 'ldaptic/adapters/active_directory_ext'
3
3
 
4
4
  module Ldaptic
5
5
  module Adapters
6
+ # Before using this adapter, try the :ldap_conn adapter. The notes below
7
+ # were originally thought to apply to all Active Directory servers, but now
8
+ # I suspect they are peculiarities of a former employer's setup. This
9
+ # adapter is a candidate for removal.
10
+ #
6
11
  # ActiveDirectoryAdapter is a LDAPConnAdapter with some Active Directory
7
12
  # specific behaviors. To help mitigate server timeout issues, this adapter
8
13
  # binds on each request and unbinds afterwards. For search requests, the
@@ -46,11 +51,10 @@ module Ldaptic
46
51
  username = [@options[:domain], username].join("\\")
47
52
  else
48
53
  conn = new_connection(3268)
49
- dn = conn.search2("", 0, "(objectClass=*", ['defaultNamingContext']).first['defaultNamingContext']
54
+ dn = conn.search2("", 0, "(objectClass=*)", ['defaultNamingContext']).first['defaultNamingContext']
50
55
  if dn
51
- domain = Ldaptic::DN(dn).rdns.map {|rdn| rdn[:dc]}.compact
52
- unless domain.empty?
53
- username = [username, domain.join(".")].join("@")
56
+ if domain = Ldaptic::DN(dn).domain
57
+ username = [username, domain].join('@')
54
58
  end
55
59
  end
56
60
  end
@@ -70,7 +74,7 @@ module Ldaptic
70
74
  end
71
75
 
72
76
  def with_writer(&block)
73
- with_port(389, &block)
77
+ with_port(@options[:port] || 389, &block)
74
78
  end
75
79
 
76
80
  end
@@ -1,3 +1,5 @@
1
+ require 'date'
2
+
1
3
  # Converts an integer representing the number of microseconds since January 1,
2
4
  # 1600 to a DateTime.
3
5
  def DateTime.microsoft(tinies)
@@ -153,10 +153,6 @@ module Ldaptic
153
153
  end
154
154
 
155
155
  def search_parameters(options = {})
156
- case options[:sort]
157
- when Proc, Method then s_attr, s_proc = nil, options[:sort]
158
- else s_attr, s_proc = options[:sort], nil
159
- end
160
156
  [
161
157
  options[:base],
162
158
  options[:scope],
@@ -165,8 +161,6 @@ module Ldaptic
165
161
  options[:attributes_only],
166
162
  options[:timeout].to_i,
167
163
  ((options[:timeout].to_f % 1) * 1e6).round,
168
- s_attr.to_s,
169
- s_proc
170
164
  ]
171
165
  end
172
166
 
@@ -31,9 +31,9 @@ module Ldaptic
31
31
  @syntax = @entry.namespace.attribute_syntax(@name)
32
32
  @target = target
33
33
  if @type.nil?
34
- @entry.logger "Unknown type for attribute #@name"
34
+ @entry.logger.warn "Unknown type for attribute #@name"
35
35
  elsif @syntax.nil?
36
- @entry.logger "Unknown syntax #{@type.syntax_oid} for attribute #{@name}"
36
+ @entry.logger.warn "Unknown syntax #{@type.syntax_oid} for attribute #{@name}"
37
37
  end
38
38
  end
39
39
 
@@ -77,15 +77,37 @@ module Ldaptic
77
77
  @target.empty?
78
78
  end
79
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)
80
+ def index(*args, &block)
81
+ if block_given? || args.size != 1
82
+ return to_a.index(*args, &block)
83
+ else
84
+ target = matchable(args.first)
85
+ @target.each_with_index do |candidate, index|
86
+ return index if matchable(candidate) == target
87
+ end
87
88
  end
88
- replace(dest)
89
+ nil
90
+ end
91
+ alias find_index index
92
+ alias rindex index
93
+
94
+ def include?(target)
95
+ !!index(target)
96
+ end
97
+
98
+ def exclude?(target)
99
+ !index(target)
100
+ end
101
+
102
+ # Like #include?, but asks the server rather than checking locally.
103
+ def compare(target)
104
+ @entry.compare(@name, target)
105
+ end
106
+
107
+ # Adds the given attributes, discarding duplicates. All arrays are
108
+ # flattened.
109
+ def add(*attributes)
110
+ replace(@target + safe_array(attributes))
89
111
  end
90
112
  alias << add
91
113
  alias concat add
@@ -102,7 +124,16 @@ module Ldaptic
102
124
  def replace(*attributes)
103
125
  attributes = safe_array(attributes)
104
126
  user_modification_guard
105
- @target.replace(attributes)
127
+ seen = {}
128
+ filtered = []
129
+ attributes.each do |value|
130
+ matchable = matchable(value)
131
+ unless seen[matchable]
132
+ filtered << value
133
+ seen[matchable] = true
134
+ end
135
+ end
136
+ @target.replace(filtered)
106
137
  self
107
138
  end
108
139
 
@@ -119,16 +150,13 @@ module Ldaptic
119
150
 
120
151
  # Remove the given attributes given, functioning more or less like
121
152
  # 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
153
  def delete(*attributes, &block)
126
154
  return clear if attributes.flatten.empty?
127
155
  dest = @target.dup
128
156
  ret = []
129
157
  safe_array(attributes).each do |attribute|
130
158
  ret << dest.delete(attribute) do
131
- match = dest.detect {|x| x.downcase == attribute.downcase}
159
+ match = dest.detect {|x| matchable(x) == matchable(attribute)}
132
160
  if match
133
161
  dest.delete(match)
134
162
  else
@@ -158,8 +186,7 @@ module Ldaptic
158
186
  alias map! collect!
159
187
 
160
188
  def insert(index, *objects)
161
- user_modification_guard
162
- @target.insert(index, *safe_array(objects))
189
+ replace(@target.dup.insert(index, *safe_array(objects)))
163
190
  self
164
191
  end
165
192
 
@@ -261,6 +288,14 @@ module Ldaptic
261
288
  end
262
289
  end
263
290
 
291
+ def matchable(value)
292
+ if @type
293
+ @type.matchable(value)
294
+ else
295
+ format(value)
296
+ end
297
+ end
298
+
264
299
  def safe_array(attributes)
265
300
  Array(attributes).flatten.compact.map {|x| format(x)}
266
301
  end
data/lib/ldaptic/dn.rb CHANGED
@@ -75,13 +75,13 @@ module Ldaptic
75
75
  filter = "(objectClass=*)"
76
76
  if source.respond_to?(:search2_ext)
77
77
  source.search2(
78
- self.to_s,
78
+ to_s,
79
79
  scope,
80
80
  filter
81
81
  )
82
82
  elsif source.respond_to?(:search)
83
83
  Array(source.search(
84
- :base => self.to_s,
84
+ :base => to_s,
85
85
  :scope => scope,
86
86
  :filter => filter,
87
87
  :limit => 1
@@ -113,6 +113,12 @@ module Ldaptic
113
113
  end
114
114
  end
115
115
 
116
+ # Join all DC elements with periods.
117
+ def domain
118
+ components = rdns.map {|rdn| rdn[:dc]}.compact
119
+ components.join('.') unless components.empty?
120
+ end
121
+
116
122
  def parent
117
123
  Ldaptic::DN(rdns[1..-1], source)
118
124
  end
@@ -142,12 +148,10 @@ module Ldaptic
142
148
  end
143
149
  end
144
150
  if other.kind_of?(Ldaptic::DN)
145
- self.rdns == other.rdns
151
+ rdns == other.rdns
146
152
  else
147
153
  super
148
154
  end
149
- # rescue
150
- # super
151
155
  end
152
156
 
153
157
  # Pass in one or more hashes to augment the DN. Otherwise, this behaves
@@ -338,7 +342,7 @@ module Ldaptic
338
342
  end
339
343
 
340
344
  def merge(hash)
341
- self.dup.update(hash)
345
+ dup.update(hash)
342
346
  end
343
347
 
344
348
  def delete(key)
data/lib/ldaptic/entry.rb CHANGED
@@ -48,11 +48,18 @@ module Ldaptic
48
48
  def create_accessors #:nodoc:
49
49
  to_be_evaled = ""
50
50
  (may(false) + must(false)).each do |attr|
51
- method = attr.to_s.tr_s('-_', '_-')
52
- to_be_evaled << <<-RUBY
53
- def #{method}() read_attribute('#{attr}').one end
54
- def #{method}=(value) write_attribute('#{attr}', value) end
55
- RUBY
51
+ if type = namespace.attribute_type(attr)
52
+ names = type.names
53
+ else
54
+ names = [attr]
55
+ end
56
+ names.each do |name|
57
+ method = name.to_s.tr_s('-_', '_-')
58
+ to_be_evaled << <<-RUBY
59
+ def #{method}() read_attribute('#{attr}').one end
60
+ def #{method}=(value) write_attribute('#{attr}', value) end
61
+ RUBY
62
+ end
56
63
  end
57
64
  class_eval(to_be_evaled, __FILE__, __LINE__)
58
65
  end
@@ -176,7 +183,7 @@ module Ldaptic
176
183
  @attributes = {}
177
184
  data = data.dup
178
185
  if dn = data.delete('dn') || data.delete(:dn)
179
- dn.first if dn.kind_of?(Array)
186
+ dn = dn.first if dn.kind_of?(Array)
180
187
  self.dn = dn
181
188
  end
182
189
  merge_attributes(data)
@@ -273,7 +280,11 @@ module Ldaptic
273
280
  # #method_missing delegates to this method, method names with underscores
274
281
  # map to attributes with hyphens.
275
282
  def read_attribute(key)
276
- key = Ldaptic.encode(key)
283
+ if type = namespace.attribute_type(key)
284
+ key = type.names.first
285
+ else
286
+ key = Ldaptic.encode(key)
287
+ end
277
288
  @attributes[key] ||= ((@original_attributes || {}).fetch(key, [])).dup
278
289
  Ldaptic::AttributeSet.new(self, key, @attributes[key])
279
290
  end
@@ -302,7 +313,7 @@ module Ldaptic
302
313
  # Changes are not committed to the server until #save is called.
303
314
  def write_attribute(key, values)
304
315
  set = read_attribute(key)
305
- if values.respond_to?(:to_str) && set.syntax_object && set.syntax_object.error("1\n1")
316
+ if values.respond_to?(:to_str) && !set.single_value? && set.syntax_object && set.syntax_object.error("1\n1")
306
317
  values = values.split(/\r?\n/)
307
318
  elsif values == ''
308
319
  values = []
@@ -357,7 +368,6 @@ module Ldaptic
357
368
  namespace.compare(dn, key, value)
358
369
  end
359
370
 
360
- # attr_reader :attributes
361
371
  def attribute_names
362
372
  attributes.keys
363
373
  end
@@ -370,25 +380,41 @@ module Ldaptic
370
380
  self['objectClass'].map {|c| namespace.object_class(c)} - self.class.ldap_ancestors
371
381
  end
372
382
 
373
- def must(all = true)
374
- self.class.must(all) + aux.map {|a|a.must(false)}.flatten
383
+ def must(include_aliases = false)
384
+ attrs = (self.class.must + aux.map {|a| a.must(false)}.flatten).uniq
385
+ if include_aliases
386
+ attrs.map do |attr|
387
+ type = namespace.attribute_type(attr)
388
+ type ? type.names : attr
389
+ end.flatten
390
+ else
391
+ attrs
392
+ end
375
393
  end
376
394
 
377
- def may(all = true)
378
- self.class.may(all) + aux.map {|a|a.may(false)}.flatten
395
+ def may(include_aliases = false)
396
+ attrs = (self.class.may + aux.map {|a| a.may(false)}.flatten).uniq - must
397
+ if include_aliases
398
+ attrs.map do |attr|
399
+ type = namespace.attribute_type(attr)
400
+ type ? type.names : attr
401
+ end.flatten
402
+ else
403
+ attrs
404
+ end
379
405
  end
380
406
 
381
407
  def may_must(attribute)
382
408
  attribute = Ldaptic.encode(attribute)
383
- if must.include?(attribute)
409
+ if must(true).include?(attribute)
384
410
  :must
385
- elsif may.include?(attribute)
411
+ elsif may(true).include?(attribute)
386
412
  :may
387
413
  end
388
414
  end
389
415
 
390
416
  def respond_to?(method, *) #:nodoc:
391
- both = may + must
417
+ both = may(true) + must(true)
392
418
  super || (both + both.map {|x| "#{x}="} + both.map {|x| "#{x}-before-type-cast"}).include?(Ldaptic.encode(method.to_sym))
393
419
  end
394
420
 
@@ -435,7 +461,10 @@ module Ldaptic
435
461
  alias find /
436
462
 
437
463
  def fetch(dn = self.dn, options = {}) #:nodoc:
438
- search({:base => dn}.merge(options))
464
+ if dn.kind_of?(Hash)
465
+ dn = self.dn/dn
466
+ end
467
+ namespace.fetch(dn, options)
439
468
  end
440
469
 
441
470
  # If a Hash or a String containing "=" is given, the argument is treated as
@@ -144,7 +144,7 @@ module Ldaptic
144
144
  def process
145
145
  "(#{@array*''})" if @array.compact.size > 1
146
146
  end
147
- def to_net_ldap_filter #:nodoc
147
+ def to_net_ldap_filter #:nodoc:
148
148
  @array[1..-1].inject {|m, o| m.to_net_ldap_filter.send(@array.first, o.to_net_ldap_filter)}
149
149
  end
150
150
  end
@@ -0,0 +1,80 @@
1
+ require 'ldaptic/syntaxes'
2
+
3
+ module Ldaptic
4
+ # RFC4517 - Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching Rules
5
+ # RFC4518 - Lightweight Directory Access Protocol (LDAP): Internationalized
6
+ # String Preparation
7
+ module MatchingRules
8
+ def self.for(name)
9
+ name = name.to_s
10
+ name = name[0..0].upcase + name[1..-1].to_s
11
+ if !name.empty? && const_defined?(name)
12
+ const_get(name)
13
+ else
14
+ CaseIgnoreMatch
15
+ end
16
+ end
17
+
18
+ class OctetStringMatch
19
+ def matchable(value)
20
+ value
21
+ end
22
+
23
+ def match(one, two)
24
+ matchable(one) == matchable(two)
25
+ end
26
+ end
27
+
28
+ class Boolean < OctetStringMatch
29
+ end
30
+
31
+ class CaseExactMatch < OctetStringMatch
32
+ def matchable(value)
33
+ super.gsub(/ +/, ' ').sub(/\A */, ' ').sub(/ *\z/, ' ')
34
+ end
35
+ end
36
+
37
+ class CaseExactIA5Match < CaseExactMatch
38
+ end
39
+
40
+ class CaseIgnoreMatch < CaseExactMatch
41
+ def matchable(value)
42
+ super.downcase
43
+ end
44
+ end
45
+
46
+ class CaseIgnoreIA5Match < CaseIgnoreMatch
47
+ end
48
+
49
+ class CaseIgnoreListMatch < CaseIgnoreMatch
50
+ end
51
+
52
+ class GeneralizedTimeMatch < OctetStringMatch
53
+ def matchable(value)
54
+ Ldaptic::Syntaxes::GeneralizedTime.parse(value)
55
+ end
56
+ end
57
+
58
+ class NumericStringMatch < OctetStringMatch
59
+ def matchable(value)
60
+ super.delete(' ')
61
+ end
62
+ end
63
+
64
+ class DistinguishedNameMatch < OctetStringMatch
65
+ def matchable(value)
66
+ Ldaptic::DN(value)
67
+ end
68
+ end
69
+
70
+ class TelephoneNumberMatch < CaseIgnoreMatch
71
+ # Doesn't remove unicode hyphen equivalents \u058A, \u2010, \u2011,
72
+ # \u2212, \ufe63, or \uff0d on account of unicode being so darn difficult
73
+ # to get right in both 1.8 and 1.9.
74
+ def matchable(value)
75
+ super.delete(' ').delete('-')
76
+ end
77
+ end
78
+
79
+ end
80
+ end