ldaptic 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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