treequel 1.2.2 → 1.3.0pre384

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'treequel'
5
+ require 'treequel/model'
6
+ require 'treequel/mixins'
7
+ require 'treequel/constants'
8
+
9
+
10
+ # Mixin that provides Treequel::Model characteristics to a mixin module.
11
+ #
12
+ # The ideas and a large portion of the implementation of this class is borrowed from
13
+ # Sequel under the following license terms:
14
+ #
15
+ # Copyright (c) 2007-2008 Sharon Rosner
16
+ # Copyright (c) 2008-2010 Jeremy Evans
17
+ #
18
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ # of this software and associated documentation files (the "Software"), to
20
+ # deal in the Software without restriction, including without limitation the
21
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
22
+ # sell copies of the Software, and to permit persons to whom the Software is
23
+ # furnished to do so, subject to the following conditions:
24
+ #
25
+ # The above copyright notice and this permission notice shall be included in
26
+ # all copies or substantial portions of the Software.
27
+ #
28
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
31
+ # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
32
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
33
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34
+ #
35
+ class Treequel::Model::Errors < ::Hash
36
+ include Treequel::HashUtilities,
37
+ Treequel::Loggable
38
+
39
+ # The word to use between attributes in error messages
40
+ ATTRIBUTE_CONJUNCTION = ' and '
41
+
42
+
43
+ ### Set the initializer block to auto-create Array values.
44
+ def initialize( *args )
45
+ block = lambda {|h,k| h[k] = [] }
46
+ super( *args, &block )
47
+ end
48
+
49
+
50
+ ### Adds an error for the given +attribute+.
51
+ ### @param [Symbol, #to_sym] attribute the attribute with an error
52
+ ### @param [String] message the description of the error condition
53
+ def add( attribute, message )
54
+ self[ attribute ] << message
55
+ end
56
+
57
+
58
+ ### Get the number of errors that have been registered.
59
+ ### @Return [Fixnum] the number of errors
60
+ def count
61
+ return self.values.inject( 0 ) {|num, val| num + val.length }
62
+ end
63
+
64
+
65
+ ### Get an Array of messages describing errors which have occurred.
66
+ ### @example
67
+ ### errors.full_messages
68
+ ### # => ['cn is not valid',
69
+ ### # 'uid is not at least 2 letters']
70
+ def full_messages
71
+ return self.inject([]) do |full_messages, (attribute, messages)|
72
+ subject = Array( attribute ).join( ATTRIBUTE_CONJUNCTION )
73
+ messages.each {|part| full_messages << "#{subject} #{part}" }
74
+ full_messages
75
+ end
76
+ end
77
+
78
+ end # class Treequel::Model::Errors
79
+
@@ -9,12 +9,12 @@ require 'treequel/constants'
9
9
 
10
10
  # Mixin that provides Treequel::Model characteristics to a mixin module.
11
11
  module Treequel::Model::ObjectClass
12
+ include Treequel::HashUtilities
13
+
12
14
 
13
15
  ### Extension callback -- add data structures to the extending +mod+.
14
16
  ### @param [Module] mod the mixin module to be extended
15
17
  def self::extended( mod )
16
- # mod.instance_variable_set( :@model_directory, nil )
17
- # mod.instance_variable_set( :@model_bases, [] )
18
18
  mod.instance_variable_set( :@model_class, Treequel::Model )
19
19
  mod.instance_variable_set( :@model_objectclasses, [] )
20
20
  mod.instance_variable_set( :@model_bases, [] )
@@ -75,6 +75,30 @@ module Treequel::Model::ObjectClass
75
75
  end
76
76
 
77
77
 
78
+ ### Instantiate a new Treequel::Model object with given +dn+ and the objectclasses
79
+ ### specified by the receiving module.
80
+ ### @param [#to_s] dn the DN of the new model object
81
+ ### @param [Hash] entryhash attributes to set on the new entry
82
+ def create( directory, dn, entryhash={} )
83
+ entryhash = stringify_keys( entryhash )
84
+
85
+ # Add the objectclasses from the mixin
86
+ entryhash['objectClass'] ||= []
87
+ entryhash['objectClass'].collect!( &:to_s )
88
+ entryhash['objectClass'] |= self.model_objectclasses.map( &:to_s )
89
+
90
+ # Add all the attribute pairs from the RDN bit of the DN to the entry
91
+ rdn_pair, _ = dn.split( /\s*,\s*/, 2 )
92
+ rdn_pair.split( /\+/ ).each do |attrpair|
93
+ k, v = attrpair.split( /\s*=\s*/ )
94
+ entryhash[ k ] ||= []
95
+ entryhash[ k ] << v
96
+ end
97
+
98
+ return self.model_class.new( directory, dn, entryhash )
99
+ end
100
+
101
+
78
102
  ### Return a Branchset (or BranchCollection if the receiver has more than one
79
103
  ### base) that can be used to search the given +directory+ for entries to which
80
104
  ### the receiver applies.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ #encoding: utf-8
3
+
4
+ require 'treequel/model'
5
+
6
+
7
+ # A collection of schema-based validations for LDAP model objects.
8
+ module Treequel::Model::SchemaValidations
9
+
10
+ ### Entrypoint -- run all the validations, adding any errors to the
11
+ ### object's #error collector.
12
+ def validate( options={} )
13
+ return unless options[:with_schema]
14
+
15
+ self.validate_must_attributes
16
+ self.validate_may_attributes
17
+ self.validate_attribute_syntax
18
+ end
19
+
20
+
21
+ ### Validate that all attributes that MUST be included according to the entry's
22
+ ### objectClasses have at least one value.
23
+ def validate_must_attributes
24
+ self.must_attribute_types.each do |attrtype|
25
+ oid = attrtype.name
26
+ if attrtype.single?
27
+ self.errors.add( oid, "MUST have a value" ) unless self[ oid ]
28
+ else
29
+ self.errors.add( oid, "MUST have at least one value" ) if self[ oid ].empty?
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ ### Validate that all attributes present in the entry are allowed by either a
36
+ ### MUST or a MAY rule of one of its objectClasses.
37
+ def validate_may_attributes
38
+ hash = (self.entry || {} ).merge( @values )
39
+ attributes = hash.keys.map( &:to_sym ).uniq
40
+ valid_attributes = self.valid_attribute_oids
41
+
42
+ self.log.debug "Validating MAY attributes: %p against the list of valid OIDs: %p" %
43
+ [ attributes, valid_attributes ]
44
+ unknown_attributes = attributes - valid_attributes
45
+ unknown_attributes.each do |oid|
46
+ self.errors.add( oid, "is not allowed by entry's objectClasses" )
47
+ end
48
+ end
49
+
50
+
51
+ ### Validate that the attribute values present in the entry are all valid according to
52
+ ### the syntax rule for it.
53
+ def validate_attribute_syntax
54
+ @values.each do |attribute, values|
55
+ [ values ].flatten.each do |value|
56
+ begin
57
+ self.get_converted_attribute( attribute.to_sym, value )
58
+ rescue => err
59
+ self.log.error "validation for %p failed: %s: %s" %
60
+ [ attribute, err.class.name, err.message ]
61
+ attrtype = self.find_attribute_type( attribute )
62
+ self.errors.add( attribute, "isn't a valid %s value" % [attrtype.syntax.desc] )
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end # module Treequel::Model::SchemaValidations
69
+
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'date'
4
+
5
+ require 'diff/lcs'
6
+ require 'diff/lcs/change'
7
+
3
8
  require 'ldap'
4
9
  require 'ldap/control'
5
10
 
@@ -57,26 +62,26 @@ module Treequel::TimeExtensions
57
62
  if fraction_digits == 0
58
63
  ''
59
64
  elsif fraction_digits <= 6
60
- '.' + sprintf('%06d', usec)[0, fraction_digits]
65
+ '.' + sprintf('%06d', self.usec)[0, fraction_digits]
61
66
  else
62
- '.' + sprintf('%06d', usec) + '0' * (fraction_digits - 6)
67
+ '.' + sprintf('%06d', self.usec) + '0' * (fraction_digits - 6)
63
68
  end
64
69
  tz =
65
70
  if self.utc?
66
71
  'Z'
67
72
  else
68
- off = utc_offset
73
+ off = self.utc_offset
69
74
  sign = off < 0 ? '-' : '+'
70
75
  "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
71
76
  end
72
77
 
73
78
  return "%02d%02d%02d%02d%02d%02d%s%s" % [
74
- year,
75
- mon,
76
- day,
77
- hour,
78
- min,
79
- sec,
79
+ self.year,
80
+ self.mon,
81
+ self.day,
82
+ self.hour,
83
+ self.min,
84
+ self.sec,
80
85
  fractional_seconds,
81
86
  tz
82
87
  ]
@@ -87,21 +92,21 @@ module Treequel::TimeExtensions
87
92
  ### (UTC Time)
88
93
  def ldap_utc
89
94
  tz =
90
- if utc?
95
+ if self.utc?
91
96
  'Z'
92
97
  else
93
- off = utc_offset
98
+ off = self.utc_offset
94
99
  sign = off < 0 ? '-' : '+'
95
100
  "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
96
101
  end
97
102
 
98
103
  return "%02d%02d%02d%02d%02d%02d%s" % [
99
- year.divmod(100).last,
100
- mon,
101
- day,
102
- hour,
103
- min,
104
- sec,
104
+ self.year.divmod(100).last,
105
+ self.mon,
106
+ self.day,
107
+ self.hour,
108
+ self.min,
109
+ self.sec,
105
110
  tz
106
111
  ]
107
112
  end
@@ -113,3 +118,80 @@ class Time
113
118
  end
114
119
 
115
120
 
121
+ ### Extensions to the Date class to add LDAP (RFC4517) Generalized Time syntax
122
+ module Treequel::DateExtensions
123
+
124
+ ### Return +self+ as a String formatted as specified in RFC4517
125
+ ### (LDAP Generalized Time).
126
+ def ldap_generalized( fraction_digits=0 )
127
+ fractional_seconds =
128
+ if fraction_digits == 0
129
+ ''
130
+ else
131
+ '.' + ('0' * fraction_digits)
132
+ end
133
+
134
+ off = Time.now.utc_offset
135
+ sign = off < 0 ? '-' : '+'
136
+ tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
137
+
138
+ return "%02d%02d%02d%02d%02d%02d%s%s" % [
139
+ self.year,
140
+ self.mon,
141
+ self.day,
142
+ 0,
143
+ 0,
144
+ 1,
145
+ fractional_seconds,
146
+ tz
147
+ ]
148
+
149
+ end
150
+
151
+ ### Returns +self+ as a String formatted as specified in RFC4517
152
+ ### (UTC Time)
153
+ def ldap_utc
154
+ off = Time.now.utc_offset
155
+ sign = off < 0 ? '-' : '+'
156
+ tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
157
+
158
+ return "%02d%02d%02d%02d%02d%02d%s" % [
159
+ self.year.divmod(100).last,
160
+ self.mon,
161
+ self.day,
162
+ 0,
163
+ 0,
164
+ 1,
165
+ tz
166
+ ]
167
+ end
168
+
169
+ end # module Treequel::TimeExtensions
170
+
171
+ class Date
172
+ include Treequel::DateExtensions
173
+ end
174
+
175
+
176
+ ### These three predicates use the wrong instance variable in the library.
177
+ ### :TODO: Submit a patch!
178
+ module Treequel::DiffLCSChangeTypeTestFixes
179
+
180
+ def changed?
181
+ @action == '!'
182
+ end
183
+
184
+ def finished_a?
185
+ @action == '>'
186
+ end
187
+
188
+ def finished_b?
189
+ @action == '<'
190
+ end
191
+
192
+ end
193
+
194
+ class Diff::LCS::ContextChange
195
+ include Treequel::DiffLCSChangeTypeTestFixes
196
+ end
197
+
@@ -209,17 +209,21 @@ class Treequel::Schema
209
209
  end
210
210
 
211
211
 
212
+ ### Return the schema as a human-readable english string.
213
+ def to_s
214
+ parts = [ "Schema:" ]
215
+ parts << self.ivar_descriptions.collect {|desc| ' ' + desc }
216
+ return parts.join( $/ )
217
+ end
218
+
219
+
212
220
  ### Return a human-readable representation of the object suitable for debugging.
213
221
  ### @return [String]
214
222
  def inspect
215
- ivar_descs = self.instance_variables.sort.collect do |ivar|
216
- len = self.instance_variable_get( ivar ).length
217
- "%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
218
- end
219
223
  return %{#<%s:0x%0x %s>} % [
220
224
  self.class.name,
221
225
  self.object_id / 2,
222
- ivar_descs.join(', '),
226
+ self.ivar_descriptions.join( ', ' ),
223
227
  ]
224
228
  end
225
229
 
@@ -308,5 +312,15 @@ class Treequel::Schema
308
312
  end
309
313
 
310
314
 
315
+ ### Return descriptions of the schema's artifacts.
316
+ ### @return [Array<String>] the descriptions of the schema's artifacts, and how many of each
317
+ ### it has.
318
+ def ivar_descriptions
319
+ self.instance_variables.sort.collect do |ivar|
320
+ len = self.instance_variable_get( ivar ).length
321
+ "%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
322
+ end
323
+ end
324
+
311
325
  end # class Treequel::Schema
312
326
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'yaml'
3
4
  require 'ldap'
4
5
  require 'treequel'
5
6
 
@@ -40,7 +41,8 @@ module Treequel::TestConstants # :nodoc:all
40
41
  "2.16.840.1.113730.3.4.2", "1.3.6.1.4.1.4203.1.10.1",
41
42
  "1.2.840.113556.1.4.319", "1.2.826.0.1.334810.2.3",
42
43
  "1.2.826.0.1.3344810.2.3", "1.3.6.1.1.13.2",
43
- "1.3.6.1.1.13.1", "1.3.6.1.1.12"
44
+ "1.3.6.1.1.13.1", "1.3.6.1.1.12",
45
+ "1.2.840.113556.1.4.473", "1.2.840.113556.1.4.474"
44
46
  ],
45
47
  "supportedExtension" => [
46
48
  "1.3.6.1.4.1.1466.20037", "1.3.6.1.4.1.4203.1.11.1",
@@ -50,6 +52,9 @@ module Treequel::TestConstants # :nodoc:all
50
52
  }]
51
53
  TEST_DSE.first.keys.each {|key| TEST_DSE.first[key].freeze }
52
54
 
55
+ SCHEMA_DUMPFILE = Pathname( __FILE__ ).dirname.parent + 'data' + 'schema.yml'
56
+ SCHEMAHASH = LDAP::Schema.new( YAML.load_file(SCHEMA_DUMPFILE) )
57
+ SCHEMA = Treequel::Schema.new( SCHEMAHASH )
53
58
 
54
59
  TEST_HOSTS_DN_ATTR = 'ou'
55
60
  TEST_HOSTS_DN_VALUE = 'Hosts'
@@ -82,7 +87,7 @@ module Treequel::TestConstants # :nodoc:all
82
87
  TEST_PEOPLE_DN = "#{TEST_PEOPLE_RDN},#{TEST_BASE_DN}"
83
88
 
84
89
  TEST_PERSON_DN_ATTR = 'uid'
85
- TEST_PERSON_DN_VALUE = 'arogers'
90
+ TEST_PERSON_DN_VALUE = 'jrandom'
86
91
  TEST_PERSON_RDN = "#{TEST_PERSON_DN_ATTR}=#{TEST_PERSON_DN_VALUE}"
87
92
  TEST_PERSON_DN = "#{TEST_PERSON_RDN},#{TEST_PEOPLE_DN}"
88
93
 
@@ -106,6 +111,19 @@ module Treequel::TestConstants # :nodoc:all
106
111
  TEST_ROOM_RDN = "#{TEST_ROOM_DN_ATTR}=#{TEST_ROOM_DN_VALUE}"
107
112
  TEST_ROOM_DN = "#{TEST_ROOM_RDN},#{TEST_ROOMS_DN}"
108
113
 
114
+ # Multivalue DN
115
+ TEST_HOST_MULTIVALUE_DN_ATTR1 = 'cn'
116
+ TEST_HOST_MULTIVALUE_DN_VALUE1 = 'honcho'
117
+ TEST_HOST_MULTIVALUE_DN_ATTR2 = 'l'
118
+ TEST_HOST_MULTIVALUE_DN_VALUE2 = 'sandiego'
119
+ TEST_HOST_MULTIVALUE_RDN = "%s=%s+%s=%s" % [
120
+ TEST_HOST_MULTIVALUE_DN_ATTR1,
121
+ TEST_HOST_MULTIVALUE_DN_VALUE1,
122
+ TEST_HOST_MULTIVALUE_DN_ATTR2,
123
+ TEST_HOST_MULTIVALUE_DN_VALUE2,
124
+ ]
125
+ TEST_HOST_MULTIVALUE_DN = "#{TEST_HOST_MULTIVALUE_RDN},#{TEST_HOSTS_DN}"
126
+
109
127
  constants.each do |cname|
110
128
  const_get(cname).freeze
111
129
  end