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,34 @@
1
+ module Ldaptic
2
+ class ErrorSet < Hash
3
+ def initialize(base)
4
+ @base = base
5
+ super() { |h, k| h[k] = [] }
6
+ end
7
+
8
+ def add(attribute, message)
9
+ self[attribute] << message
10
+ end
11
+
12
+ def each
13
+ each_key do |attribute|
14
+ self[attribute].each do |message|
15
+ yield attribute, message
16
+ end
17
+ end
18
+ end
19
+
20
+ def full_messages
21
+ map do |attribute, message|
22
+ "#{@base.class.human_attribute_name(attribute)} #{message}"
23
+ end
24
+ end
25
+
26
+ def to_a
27
+ full_messages
28
+ end
29
+
30
+ def size
31
+ full_messages.size
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,136 @@
1
+ module Ldaptic
2
+
3
+ class Error < ::RuntimeError
4
+ end
5
+
6
+ class EntryNotSaved < Error
7
+ end
8
+
9
+ # All server errors are instances of this class. The error message and error
10
+ # code can be accessed with <tt>exception.message</tt> and
11
+ # <tt>exception.code</tt> respectively.
12
+ class ServerError < Error
13
+ attr_accessor :code
14
+ end
15
+
16
+ # The module houses all subclasses of Ldaptic::ServerError. The methods
17
+ # contained within are for internal use only.
18
+ module Errors
19
+
20
+ #{
21
+ # 0=>"Success",
22
+ # 1=>"Operations error",
23
+ # 2=>"Protocol error",
24
+ # 3=>"Time limit exceeded",
25
+ # 4=>"Size limit exceeded",
26
+ # 5=>"Compare False",
27
+ # 6=>"Compare True",
28
+ # 7=>"Authentication method not supported"
29
+ # 8=>"Strong(er) authentication required",
30
+ # 9=>"Partial results and referral received",
31
+ # 10=>"Referral",
32
+ # 11=>"Administrative limit exceeded",
33
+ # 12=>"Critical extension is unavailable",
34
+ # 13=>"Confidentiality required",
35
+ # 14=>"SASL bind in progress",
36
+ # 16=>"No such attribute",
37
+ # 17=>"Undefined attribute type",
38
+ # 18=>"Inappropriate matching",
39
+ # 19=>"Constraint violation",
40
+ # 20=>"Type or value exists",
41
+ # 21=>"Invalid syntax",
42
+ # 32=>"No such object",
43
+ # 33=>"Alias problem",
44
+ # 34=>"Invalid DN syntax",
45
+ # 35=>"Entry is a leaf",
46
+ # 36=>"Alias dereferencing problem",
47
+ # 47=>"Proxy Authorization Failure",
48
+ # 48=>"Inappropriate authentication",
49
+ # 49=>"Invalid credentials",
50
+ # 50=>"Insufficient access",
51
+ # 51=>"Server is busy",
52
+ # 52=>"Server is unavailable",
53
+ # 53=>"Server is unwilling to perform",
54
+ # 54=>"Loop detected",
55
+ # 64=>"Naming violation",
56
+ # 65=>"Object class violation",
57
+ # 66=>"Operation not allowed on non-leaf",
58
+ # 67=>"Operation not allowed on RDN",
59
+ # 68=>"Already exists",
60
+ # 69=>"Cannot modify object class",
61
+ # 70=>"Results too large",
62
+ # 71=>"Operation affects multiple DSAs",
63
+ # 80=>"Internal (implementation specific) error",
64
+ # 81=>"Can't contact LDAP server",
65
+ # 82=>"Local error",
66
+ # 83=>"Encoding error",
67
+ # 84=>"Decoding error",
68
+ # 85=>"Timed out",
69
+ # 86=>"Unknown authentication method",
70
+ # 87=>"Bad search filter",
71
+ # 88=>"User cancelled operation",
72
+ # 89=>"Bad parameter to an ldap routine",
73
+ # 90=>"Out of memory",
74
+ # 91=>"Connect error",
75
+ # 92=>"Not Supported",
76
+ # 93=>"Control not found",
77
+ # 94=>"No results returned",
78
+ # 95=>"More results to return",
79
+ # 96=>"Client Loop",
80
+ # 97=>"Referral Limit Exceeded",
81
+ #}
82
+
83
+ # Error code 32.
84
+ class NoSuchObject < ServerError
85
+ end
86
+
87
+ # Error code 5.
88
+ class CompareFalse < ServerError
89
+ end
90
+ # Error code 6.
91
+ class CompareTrue < ServerError
92
+ end
93
+
94
+ EXCEPTIONS = {
95
+ 32 => NoSuchObject,
96
+ 5 => CompareFalse,
97
+ 6 => CompareTrue
98
+ }
99
+
100
+ class << self
101
+
102
+ # Provides a backtrace minus all files shipped with Ldaptic.
103
+ def application_backtrace
104
+ dir = File.dirname(File.dirname(__FILE__))
105
+ c = caller
106
+ c.shift while c.first[0,dir.length] == dir
107
+ c
108
+ end
109
+
110
+ # Raise an exception (object only, no strings or classes) with the
111
+ # backtrace stripped of all Ldaptic files.
112
+ def raise(exception)
113
+ exception.set_backtrace(application_backtrace)
114
+ Kernel.raise exception
115
+ end
116
+
117
+ def for(code, message = nil) #:nodoc:
118
+ message ||= "Unknown error #{code}"
119
+ klass = EXCEPTIONS[code] || ServerError
120
+ exception = klass.new(message)
121
+ exception.code = code
122
+ exception
123
+ end
124
+
125
+ # Given an error code and a message, raise an Ldaptic::ServerError unless
126
+ # the code is zero. The right subclass is selected automatically if it
127
+ # is available.
128
+ def raise_unless_zero(code, message = nil)
129
+ raise self.for(code, message) unless code.zero?
130
+ end
131
+
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,110 @@
1
+ module Ldaptic
2
+
3
+ # Encode an object with LDAP semantics. Generally this is just to_s, but
4
+ # dates and booleans get special treatment.
5
+ #
6
+ # If a symbol is passed in, underscores are replaced by dashes, aiding in
7
+ # bridging the gap between LDAP and Ruby conventions.
8
+ def self.encode(value)
9
+ if value.respond_to?(:utc)
10
+ value.dup.utc.strftime("%Y%m%d%H%M%S") + ".%06dZ" % value.usec
11
+ elsif [true, false].include?(value)
12
+ value.to_s.upcase
13
+ elsif value.respond_to?(:dn)
14
+ value.dn.dup
15
+ elsif value.kind_of?(Symbol)
16
+ value.to_s.gsub('_', '-')
17
+ else
18
+ value.to_s.dup
19
+ end
20
+ end
21
+
22
+ # Escape a string for use in an LDAP filter, or in a DN. If the second
23
+ # argument is +true+, asterisks are not escaped.
24
+ #
25
+ # If the first argument is not a string, it is handed off to LDAP::encode.
26
+ def self.escape(string, allow_asterisks = false)
27
+ string = Ldaptic.encode(string)
28
+ enc = lambda { |l| "\\%02X" % l.ord }
29
+ string.gsub!(/[()\\\0-\37"+,;<>]/, &enc)
30
+ string.gsub!(/\A[# ]| \Z/, &enc)
31
+ if allow_asterisks
32
+ string.gsub!('**', '\\\\2A')
33
+ else
34
+ string.gsub!('*', '\\\\2A')
35
+ end
36
+ string
37
+ end
38
+
39
+ def self.unescape(string)
40
+ dest = ""
41
+ string = string.strip # Leading and trailing whitespace MUST be encoded
42
+ if string[0,1] == "#"
43
+ [string[1..-1]].pack("H*")
44
+ else
45
+ backslash = nil
46
+ string.each_byte do |byte|
47
+ case backslash
48
+ when true
49
+ char = byte.chr
50
+ if ('0'..'9').include?(char) || ('a'..'f').include?(char.downcase)
51
+ backslash = char
52
+ else
53
+ dest << byte
54
+ backslash = nil
55
+ end
56
+
57
+ when String
58
+ dest << (backslash << byte).to_i(16)
59
+ backslash = nil
60
+
61
+ else
62
+ backslash = nil
63
+ if byte == 92 # ?\\
64
+ backslash = true
65
+ else
66
+ dest << byte
67
+ end
68
+ end
69
+ end
70
+ dest
71
+ end
72
+ end
73
+
74
+ # Split on a given character where it is not escaped. Either an integer or
75
+ # string represenation of the character may be used.
76
+ #
77
+ # Ldaptic.split("a*b", '*') # => ["a","b"]
78
+ # Ldaptic.split("a\\*b", '*') # => ["a\\*b"]
79
+ # Ldaptic.split("a\\\\*b", ?*) # => ["a\\\\","b"]
80
+ def self.split(string, character)
81
+ return [] if string.empty?
82
+ array = [""]
83
+ character = character.to_str.ord if character.respond_to?(:to_str)
84
+ backslash = false
85
+
86
+ string.each_byte do |byte|
87
+ if backslash
88
+ array.last << byte
89
+ backslash = false
90
+ elsif byte == 92 # ?\\
91
+ array.last << byte
92
+ backslash = true
93
+ elsif byte == character
94
+ array << ""
95
+ else
96
+ array.last << byte
97
+ end
98
+ end
99
+ array
100
+ end
101
+
102
+ end
103
+
104
+ class String
105
+ unless method_defined?(:ord)
106
+ def ord
107
+ self[0].to_i
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,282 @@
1
+ require 'ldaptic/escape'
2
+
3
+ module Ldaptic
4
+
5
+ # If the argument is already a valid Ldaptic::Filter object, return it
6
+ # untouched. Otherwise, pass it to the appropriate constructer of the
7
+ # appropriate subclass.
8
+ #
9
+ # Ldaptic::Filter("(cn=Wu*)").to_s #=> '(cn=Wu*)'
10
+ # Ldaptic::Filter({:cn=>"Wu*"}).to_s #=> '(cn=Wu\2A)'
11
+ # Ldaptic::Filter(["(cn=?*)","Wu*"]).to_s #=> '(cn=Wu\2A*)'
12
+ def self.Filter(argument)
13
+ case argument
14
+ when Filter::Abstract then argument
15
+ when [],nil then nil
16
+ when Array then Filter::Array .new(argument)
17
+ when Hash then Filter::Hash .new(argument)
18
+ when String then Filter::String .new(argument)
19
+ when Symbol then Filter::Attribute.new(argument)
20
+ when Proc, Method
21
+ Ldaptic::Filter(if argument.arity > 0
22
+ argument.call(Filter::Spawner)
23
+ elsif Filter::Spawner.respond_to?(:instance_exec)
24
+ Filter::Spawner.instance_exec(&argument)
25
+ else
26
+ Filter::Spawner.instance_eval(&argument)
27
+ end)
28
+ else raise TypeError, "Unknown LDAP Filter type", caller
29
+ end
30
+ end
31
+
32
+ # See Ldaptic.Filter for the contructor and Ldaptic::Filter::Abstract for
33
+ # methods common to all filters.
34
+ #
35
+ # Useful subclasses include String, Array, and Hash.
36
+ module Filter
37
+
38
+ # The filter class from which all others derive.
39
+ class Abstract
40
+
41
+ # Combine two filters with a logical AND.
42
+ def &(other)
43
+ And.new(self, other)
44
+ end
45
+
46
+ # Combine two filters with a logical OR.
47
+ def |(other)
48
+ Or.new(self, other)
49
+ end
50
+
51
+ # Negate a filter.
52
+ #
53
+ # ~Ldaptic::Filter("(a=1)").to_s # => "(!(a=1))"
54
+ def ~
55
+ Not.new(self)
56
+ end
57
+
58
+ # Generates the filter as a string.
59
+ def to_s
60
+ process || "(objectClass=*)"
61
+ end
62
+
63
+ alias to_str to_s
64
+
65
+ def inspect
66
+ if string = process
67
+ "#<#{Ldaptic::Filter.inspect} #{string}>"
68
+ else
69
+ "#<#{Ldaptic::Filter.inspect} invalid>"
70
+ end
71
+ end
72
+
73
+ def to_net_ldap_filter #:nodoc:
74
+ Net::LDAP::Filter.construct(process)
75
+ end
76
+
77
+ def to_ber #:nodoc:
78
+ to_net_ldap_filter.to_ber
79
+ end
80
+
81
+ end
82
+
83
+ module Spawner # :nodoc:
84
+ def self.method_missing(method)
85
+ Attribute.new(method)
86
+ end
87
+ end
88
+
89
+ class Attribute < Abstract
90
+ def initialize(name)
91
+ if name.kind_of?(Symbol)
92
+ name = name.to_s.tr('_-', '-_')
93
+ end
94
+ @name = name
95
+ end
96
+ %w(== =~ >= <=).each do |method|
97
+ define_method(method) do |other|
98
+ Pair.new(@name, other, method)
99
+ end
100
+ end
101
+ def process
102
+ "(#{@name}=*)"
103
+ end
104
+ end
105
+
106
+ # This class is used for raw LDAP queries. Note that the outermost set of
107
+ # parentheses *must* be used.
108
+ #
109
+ # Ldaptic::Filter("a=1") # Wrong
110
+ # Ldaptic::Filter("(a=1)") # Correct
111
+ class String < Abstract
112
+
113
+ def initialize(string) #:nodoc:
114
+ @string = string
115
+ end
116
+
117
+ # Returns the original string
118
+ def process
119
+ @string
120
+ end
121
+
122
+ end
123
+
124
+ # Does ? parameter substitution.
125
+ #
126
+ # Ldaptic::Filter(["(cn=?*)", "Sm"]).to_s #=> "(cn=Sm*)"
127
+ class Array < Abstract
128
+ def initialize(array) #:nodoc:
129
+ @template = array.first
130
+ @parameters = array[1..-1]
131
+ end
132
+ def process
133
+ parameters = @parameters.dup
134
+ string = @template.gsub('?') { Ldaptic.escape(parameters.pop) }
135
+ end
136
+ end
137
+
138
+ # Used in the implementation of Ldaptic::Filter::And and
139
+ # Ldaptic::Filter::Or. For internal use only.
140
+ class Join < Abstract
141
+ def initialize(operator, *args) #:nodoc:
142
+ @array = [operator] + args.map {|arg| Ldaptic::Filter(arg)}
143
+ end
144
+ def process
145
+ "(#{@array*''})" if @array.compact.size > 1
146
+ end
147
+ def to_net_ldap_filter #:nodoc
148
+ @array[1..-1].inject {|m, o| m.to_net_ldap_filter.send(@array.first, o.to_net_ldap_filter)}
149
+ end
150
+ end
151
+
152
+ class And < Join
153
+ def initialize(*args)
154
+ super(:&, *args)
155
+ end
156
+ end
157
+
158
+ class Or < Join
159
+ def initialize(*args)
160
+ super(:|, *args)
161
+ end
162
+ end
163
+
164
+ class Not < Abstract
165
+ def initialize(object)
166
+ @object = Ldaptic::Filter(object)
167
+ end
168
+ def process
169
+ process = @object.process and "(!#{process})"
170
+ end
171
+ def to_net_ldap_filter #:nodoc:
172
+ ~ @object.to_net_ldap_filter
173
+ end
174
+ end
175
+
176
+ # A hash is the most general and most useful type of filter builder.
177
+ #
178
+ # Ldaptic::Filter(
179
+ # :givenName => "David",
180
+ # :sn! => "Thomas",
181
+ # :postalCode => (70000..80000)
182
+ # ).to_s # => "(&(givenName=David)(&(postalCode>=70000)(postalCode<=80000))(!(sn=Thomas)))"
183
+ #
184
+ # Including :* => true allows asterisks to pass through unaltered.
185
+ # Otherwise, they are escaped.
186
+ #
187
+ # Ldaptic::Filter(:givenName => "Dav*", :* => true).to_s # => "(givenName=Dav*)"
188
+ class Hash < Abstract
189
+
190
+ attr_accessor :escape_asterisks
191
+ attr_reader :hash
192
+ # Call Ldaptic::Filter(hash) instead of instantiating this class
193
+ # directly.
194
+ def initialize(hash)
195
+ @hash = hash.dup
196
+ @escape_asterisks = !@hash.delete(:*)
197
+ end
198
+
199
+ def process
200
+ string = @hash.map {|k, v| [k.to_s, v]}.sort.map do |(k, v)|
201
+ Pair.new(k, v, @escape_asterisks ? "==" : "=~").process
202
+ end.join
203
+ case @hash.size
204
+ when 0 then nil
205
+ when 1 then string
206
+ else "(&#{string})"
207
+ end
208
+ end
209
+ end
210
+
211
+ # Internal class used to process a single entry from a hash.
212
+ class Pair < Abstract
213
+ INVERSE_OPERATORS = {
214
+ "!=" => "==",
215
+ "!~" => "=~",
216
+ ">" => "<=",
217
+ "<" => ">="
218
+ }
219
+ def initialize(key, value, operator)
220
+ @key, @value, @operator = key.to_s.dup, value, operator.to_s
221
+ @inverse = !!@key.sub!(/!$/, '')
222
+ if op = INVERSE_OPERATORS[@operator]
223
+ @inverse ^= true
224
+ @operator = op
225
+ end
226
+ end
227
+
228
+ def process
229
+ k = @key
230
+ v = @value
231
+ if @operator == "=~"
232
+ operator = "=="
233
+ star = true
234
+ else
235
+ operator = @operator
236
+ star = false
237
+ end
238
+ inverse = @inverse
239
+ operator = "=" if operator == "=="
240
+ if v.respond_to?(:to_ary)
241
+ q = "(|" + v.map {|e| "(#{Ldaptic.encode(k)}=#{Ldaptic.escape(e, star)})"}.join + ")"
242
+ elsif v.kind_of?(Range)
243
+ q = []
244
+ if v.first != -1.0/0
245
+ q << "(#{Ldaptic.encode(k)}>=#{Ldaptic.escape(v.first, star)})"
246
+ end
247
+ if v.last != 1.0/0
248
+ if v.exclude_end?
249
+ q << "(!(#{Ldaptic.encode(k)}>=#{Ldaptic.escape(v.last, star)}))"
250
+ else
251
+ q << "(#{Ldaptic.encode(k)}<=#{Ldaptic.escape(v.last, star)})"
252
+ end
253
+ end
254
+ q = "(&#{q*""})"
255
+ elsif v == true || v == :*
256
+ q = "(#{Ldaptic.encode(k)}=*)"
257
+ elsif !v
258
+ q = "(#{Ldaptic.encode(k)}=*)"
259
+ inverse ^= true
260
+ else
261
+ q = "(#{Ldaptic.encode(k)}#{operator}#{Ldaptic.escape(v, star)})"
262
+ end
263
+ inverse ? "(!#{q})" : q
264
+ end
265
+ end
266
+
267
+ module Conversions #:nodoc:
268
+ def to_ldap_filter
269
+ Ldaptic::Filter(self)
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+ end
276
+
277
+ class Hash
278
+ include Ldaptic::Filter::Conversions
279
+ end
280
+ class String
281
+ include Ldaptic::Filter::Conversions
282
+ end