ruby-ldapserver 0.3.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.
@@ -0,0 +1,93 @@
1
+ require 'prefork' # <http://raa.ruby-lang.org/project/prefork/>
2
+ require 'socket'
3
+
4
+ module LDAP
5
+ class Server
6
+
7
+ # Accept connections on a port, and for each one run the given block
8
+ # in one of N pre-forked children. Returns a Thread object for the
9
+ # listener.
10
+ #
11
+ # Options:
12
+ # :port=>port number [required]
13
+ # :bindaddr=>"IP address"
14
+ # :user=>"username" - drop privileges after bind
15
+ # :group=>"groupname" - ditto
16
+ # :logger=>object - implements << method
17
+ # :listen=>number - listen queue depth
18
+ # :nodelay=>true - set TCP_NODELAY option
19
+ # :min_servers=>N - prefork parameters
20
+ # :max_servers=>N
21
+ # :max_requests_per_child=>N
22
+ # :max_idle=>N - seconds
23
+
24
+ def self.preforkserver(opt, &blk)
25
+ logger = opt[:logger] || $stderr
26
+ server = PreFork.new(opt[:bindaddr] || "0.0.0.0", opt[:port])
27
+
28
+ # Drop privileges if requested
29
+ if opt[:group] or opt[:user]
30
+ require 'etc'
31
+ gid = Etc.getgrnam(opt[:group]).gid if opt[:group]
32
+ uid = Etc.getpwnam(opt[:user]).uid if opt[:user]
33
+ File.chown(uid, gid, server.instance_eval {@lockf})
34
+ Process.gid = Process.egid = gid if gid
35
+ Process.uid = Process.euid = uid if uid
36
+ end
37
+
38
+ # Typically the O/S will buffer response data for 100ms before sending.
39
+ # If the response is sent as a single write() then there's no need for it.
40
+ if opt[:nodelay]
41
+ begin
42
+ server.sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
43
+ rescue Exception
44
+ end
45
+ end
46
+ # set queue size for incoming connections (default is 5)
47
+ server.sock.listen(opt[:listen]) if opt[:listen]
48
+
49
+ # Set prefork server parameters
50
+ server.min_servers = opt[:min_servers] if opt[:min_servers]
51
+ server.max_servers = opt[:max_servers] if opt[:max_servers]
52
+ server.max_request_per_child = opt[:max_request_per_child] if opt[:max_request_per_child]
53
+ server.max_idle = opt[:max_idle] if opt[:max_idle]
54
+
55
+ Thread.new do
56
+ server.start do |s|
57
+ begin
58
+ s.instance_eval(&blk)
59
+ rescue Interrupt
60
+ # This exception can be raised to shut the server down
61
+ server.stop
62
+ rescue Exception => e
63
+ logger << "[#{s.peeraddr[3]}]: #{e}: #{e.backtrace[0]}\n"
64
+ ensure
65
+ s.close
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ end # class Server
72
+ end # module LDAP
73
+
74
+ if __FILE__ == $0
75
+ # simple test
76
+ puts "Running a test POP3 server on port 1110"
77
+ t = LDAP::Server.preforkserver(:port=>1110) do
78
+ print "+OK I am a fake POP3 server (pid #{$$})\r\n"
79
+ while line = gets
80
+ case line
81
+ when /^quit/i
82
+ break
83
+ when /^crash/i
84
+ raise Errno::EPERM, "dammit!"
85
+ else
86
+ print "-ERR I don't understand #{line}"
87
+ end
88
+ end
89
+ print "+OK bye\r\n"
90
+ end
91
+ #sleep 10; t.raise Interrupt # uncomment to run for fixed time period
92
+ t.join
93
+ end
@@ -0,0 +1,71 @@
1
+ module LDAP
2
+
3
+ # compatible with ruby-ldap
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ResultError < Error
8
+ end
9
+
10
+ # This exception is raised when we need to kill an existing Operation
11
+ # thread because of a received abandonRequest or bindRequest
12
+ class Abandon < Interrupt
13
+ end
14
+
15
+ # ResultError constants from RFC 2251 4.1.10; these are all exceptions
16
+ # which can be raised
17
+
18
+ class ResultError
19
+ class Success < self; def to_i; 0; end; end
20
+ class OperationsError < self; def to_i; 1; end; end
21
+ class ProtocolError < self; def to_i; 2; end; end
22
+ class TimeLimitExceeded < self; def to_i; 3; end; end
23
+ class SizeLimitExceeded < self; def to_i; 4; end; end
24
+ class CompareFalse < self; def to_i; 5; end; end
25
+ class CompareTrue < self; def to_i; 6; end; end
26
+ class AuthMethodNotSupported < self; def to_i; 7; end; end
27
+ class StrongAuthRequired < self; def to_i; 8; end; end
28
+ class Referral < self; def to_i; 10; end; end
29
+ class AdminLimitExceeded < self; def to_i; 11; end; end
30
+ class UnavailableCriticalExtension < self; def to_i; 12; end; end
31
+ class ConfidentialityRequired < self; def to_i; 13; end; end
32
+ class SaslBindInProgress < self; def to_i; 14; end; end
33
+ class NoSuchAttribute < self; def to_i; 16; end; end
34
+ class UndefinedAttributeType < self; def to_i; 17; end; end
35
+ class InappropriateMatching < self; def to_i; 18; end; end
36
+ class ConstraintViolation < self; def to_i; 19; end; end
37
+ class AttributeOrValueExists < self; def to_i; 20; end; end
38
+ class InvalidAttributeSyntax < self; def to_i; 21; end; end
39
+ class NoSuchObject < self; def to_i; 32; end; end
40
+ class AliasProblem < self; def to_i; 33; end; end
41
+ class InvalidDNSyntax < self; def to_i; 34; end; end
42
+ class IsLeaf < self; def to_i; 35; end; end
43
+ class AliasDereferencingProblem < self; def to_i; 36; end; end
44
+ class InappropriateAuthentication < self; def to_i; 48; end; end
45
+ class InvalidCredentials < self; def to_i; 49; end; end
46
+ class InsufficientAccessRights < self; def to_i; 50; end; end
47
+ class Busy < self; def to_i; 51; end; end
48
+ class Unavailable < self; def to_i; 52; end; end
49
+ class UnwillingToPerform < self; def to_i; 53; end; end
50
+ class LoopDetect < self; def to_i; 54; end; end
51
+ class NamingViolation < self; def to_i; 64; end; end
52
+ class ObjectClassViolation < self; def to_i; 65; end; end
53
+ class NotAllowedOnNonLeaf < self; def to_i; 66; end; end
54
+ class NotAllowedOnRDN < self; def to_i; 67; end; end
55
+ class EntryAlreadyExists < self; def to_i; 68; end; end
56
+ class ObjectClassModsProhibited < self; def to_i; 69; end; end
57
+ class AffectsMultipleDSAs < self; def to_i; 71; end; end
58
+ class Other < self; def to_i; 80; end; end
59
+
60
+ # Reverse lookup: so you can do raise LDAP::ResultError[53]
61
+
62
+ N_TO_CLASS = {
63
+ 53 => UnwillingToPerform,
64
+ # FIXME: please fill in the rest
65
+ }
66
+ def self.[] (n)
67
+ return N_TO_CLASS[n] || self
68
+ end
69
+ end # class ResultError
70
+
71
+ end # module LDAP
@@ -0,0 +1,592 @@
1
+ require 'ldap/server/syntax'
2
+ require 'ldap/server/result'
3
+
4
+ module LDAP
5
+ class Server
6
+
7
+ # This object represents an LDAP schema: that is, a collection of
8
+ # objectclasses and attributetypes. Methods are provided for loading
9
+ # the schema (from a string or a disk file), and validating an av-hash
10
+ # against it.
11
+
12
+ class Schema
13
+
14
+ SUBSCHEMA_ENTRY_ATTR = 'cn'
15
+ SUBSCHEMA_ENTRY_VALUE = 'Subschema'
16
+
17
+ def initialize
18
+ @attrtypes = {} # name/alias/oid => AttributeType instance
19
+ @objectclasses = {} # name/alias/oid => ObjectClass instance
20
+ @subschema_cache = nil
21
+ end
22
+
23
+ # return the DN of the subschema subentry
24
+
25
+ def subschema_dn
26
+ "#{SUBSCHEMA_ENTRY_ATTR}=#{SUBSCHEMA_ENTRY_VALUE}"
27
+ end
28
+
29
+ # Return an av hash object giving the subschema subentry. This is cached, so
30
+ # call Schema#changed if it needs to be rebuilt
31
+
32
+ def subschema_subentry
33
+ @subschema_cache ||= {
34
+ 'objectClass' => ['top','subschema','extensibleObject'],
35
+ SUBSCHEMA_ENTRY_ATTR => [SUBSCHEMA_ENTRY_VALUE],
36
+ 'objectClasses' => all_objectclasses.collect { |s| s.to_def },
37
+ 'attributeTypes' => all_attrtypes.collect { |s| s.to_def },
38
+ 'ldapSyntaxes' => LDAP::Server::Syntax.all_syntaxes.collect { |s| s.to_def },
39
+ #'matchingRules' =>
40
+ #'matchingRuleUse' =>
41
+ }
42
+ end
43
+
44
+ # Clear the subschema subentry cache, so the next time someone requests
45
+ # it, it will be rebuilt
46
+
47
+ def changed
48
+ @subschema_cache = nil
49
+ end
50
+
51
+ # Add an AttributeType to the schema
52
+
53
+ def add_attrtype(str)
54
+ a = AttributeType.new(str)
55
+ @attrtypes[a.oid] = a if a.oid
56
+ a.names.each do |n|
57
+ @attrtypes[n.downcase] = a
58
+ end
59
+ end
60
+
61
+ # Locate an attributetype object by name/alias/oid (or raise exception)
62
+
63
+ def find_attrtype(n)
64
+ return n if n.nil? or n.is_a?(LDAP::Server::Schema::AttributeType)
65
+ r = @attrtypes[n.downcase]
66
+ raise LDAP::ResultError::UndefinedAttributeType, "Unknown AttributeType #{n.inspect}" unless r
67
+ r
68
+ end
69
+
70
+ # Return array of all AttributeType objects in this schema
71
+
72
+ def all_attrtypes
73
+ @attrtypes.values.uniq
74
+ end
75
+
76
+ # Add an ObjectClass to the schema
77
+
78
+ def add_objectclass(str)
79
+ o = ObjectClass.new(str)
80
+ @objectclasses[o.oid] = o if o.oid
81
+ o.names.each do |n|
82
+ @objectclasses[n.downcase] = o
83
+ end
84
+ end
85
+
86
+ # Locate an objectclass object by name/alias/oid (or raise exception)
87
+
88
+ def find_objectclass(n)
89
+ return n if n.nil? or n.is_a?(LDAP::Server::Schema::ObjectClass)
90
+ r = @objectclasses[n.downcase]
91
+ raise LDAP::ResultError::ObjectClassViolation, "Unknown ObjectClass #{n.inspect}" unless r
92
+ r
93
+ end
94
+
95
+ # Return array of all ObjectClass objects in this schema
96
+
97
+ def all_objectclasses
98
+ @objectclasses.values.uniq
99
+ end
100
+
101
+ # Load an OpenLDAP-format schema from a named file (see notes under 'load')
102
+
103
+ def load_file(filename)
104
+ File.open(filename) { |f| load(f) }
105
+ end
106
+
107
+ # Load an OpenLDAP-format schema from a string or IO object (anything
108
+ # which responds to 'each_line'). Lines starting 'attributetype'
109
+ # or 'objectclass' contain one of those objects. Does not implement
110
+ # named objectIdentifier prefixes (used in the dyngroup.schema file
111
+ # supplied with openldap, but not documented in RFC2252)
112
+ #
113
+ # Note: RFC2252 is strict about the order in which the elements appear,
114
+ # and so are we, but OpenLDAP is not. This means that a schema which
115
+ # works in OpenLDAP might not load here. For example, RFC2252 says
116
+ # that in an objectclass description, "SUP" must come before "MAY";
117
+ # if they are the other way round, our regexp-based parser will not
118
+ # accept it. The solution is simply to modify the definition so that
119
+ # the elements appear in the correct order.
120
+
121
+ def load(str_or_io)
122
+ meth = :junk_line
123
+ data = ""
124
+ str_or_io.each_line do |line|
125
+ case line
126
+ when /^\s*#/, /^\s*$/
127
+ next
128
+ when /^objectclass\s*(.*)$/i
129
+ m = $~
130
+ send(meth, data)
131
+ meth, data = :add_objectclass, m[1]
132
+ when /^attributetype\s*(.*)$/i
133
+ m = $~
134
+ send(meth, data)
135
+ meth, data = :add_attrtype, m[1]
136
+ else
137
+ data << line
138
+ end
139
+ end
140
+ send(meth,data)
141
+ self
142
+ end
143
+
144
+ def junk_line(data)
145
+ return if data.empty?
146
+ raise LDAP::ResultError::InvalidAttributeSyntax,
147
+ "Expected 'attributetype' or 'objectclass', got #{data}"
148
+ end
149
+ private :junk_line
150
+
151
+ # Load in the base set of objectclasses and attributetypes, being
152
+ # the same set as OpenLDAP preloads internally. Includes objectclasses
153
+ # 'top', 'objectclass'; attributetypes 'objectclass' , 'cn',
154
+ # 'userPassword' and 'distinguishedName'; common operational attributes
155
+ # such as 'modifyTimestamp'; plus extras needed for publishing a v3
156
+ # schema via LDAP
157
+
158
+ def load_system
159
+ load(<<EOS)
160
+ attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'RFC2079: Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
161
+ attributetype ( 2.5.4.35 NAME 'userPassword' DESC 'RFC2256/2307: password of user' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )
162
+ attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' ) DESC 'RFC2256: common name(s) for which the entity is known by' SUP name )
163
+ attributetype ( 2.5.4.41 NAME 'name' DESC 'RFC2256: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
164
+ attributetype ( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC2256: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
165
+ attributetype ( 2.16.840.1.113730.3.1.34 NAME 'ref' DESC 'namedref: subordinate referral URL' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE distributedOperation )
166
+ attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' ) DESC 'RFC2256: name of aliased object' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )
167
+ attributetype ( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC2252: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )
168
+ attributetype ( 2.5.21.8 NAME 'matchingRuleUse' DESC 'RFC2252: matching rule uses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )
169
+ attributetype ( 2.5.21.6 NAME 'objectClasses' DESC 'RFC2252: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )
170
+ attributetype ( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC2252: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )
171
+ attributetype ( 2.5.21.4 NAME 'matchingRules' DESC 'RFC2252: matching rules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )
172
+ attributetype ( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
173
+ attributetype ( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )
174
+ attributetype ( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' DESC 'features supported by the server' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
175
+ attributetype ( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' DESC 'RFC2252: supported SASL mechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )
176
+ attributetype ( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC2252: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )
177
+ attributetype ( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' DESC 'RFC2252: supported extended operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
178
+ attributetype ( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' DESC 'RFC2252: supported controls' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )
179
+ attributetype ( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC2252: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )
180
+ attributetype ( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' DESC 'RFC2252: alternative servers' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )
181
+ attributetype ( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC2252: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
182
+ attributetype ( 2.5.18.9 NAME 'hasSubordinates' DESC 'X.501: entry has children' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
183
+ attributetype ( 2.5.18.4 NAME 'modifiersName' DESC 'RFC2252: name of last modifier' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
184
+ attributetype ( 2.5.18.3 NAME 'creatorsName' DESC 'RFC2252: name of creator' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
185
+ attributetype ( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC2252: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
186
+ attributetype ( 2.5.18.1 NAME 'createTimestamp' DESC 'RFC2252: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
187
+ attributetype ( 2.5.21.9 NAME 'structuralObjectClass' DESC 'X.500(93): structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )
188
+ attributetype ( 2.5.4.0 NAME 'objectClass' DESC 'RFC2256: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
189
+ # These ones aren't published by OpenLDAP, but are referenced by the 'subschema' objectclass
190
+ attributetype ( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation )
191
+ attributetype ( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation )
192
+ attributetype ( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation )
193
+
194
+ objectclass ( 2.5.20.1 NAME 'subschema' DESC 'RFC2252: controlling subschema (sub)entry' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )
195
+ #Don't have definition for subtreeSpecification:
196
+ #objectClass ( 2.5.17.0 NAME 'subentry' SUP top STRUCTURAL MUST ( cn $ subtreeSpecification ) )
197
+ objectClass ( 1.3.6.1.4.1.4203.1.4.1 NAME ( 'OpenLDAProotDSE' 'LDAProotDSE' ) DESC 'OpenLDAP Root DSE object' SUP top STRUCTURAL MAY cn )
198
+ objectClass ( 2.16.840.1.113730.3.2.6 NAME 'referral' DESC 'namedref: named subordinate referral' SUP top STRUCTURAL MUST ref )
199
+ objectClass ( 2.5.6.1 NAME 'alias' DESC 'RFC2256: an alias' SUP top STRUCTURAL MUST aliasedObjectName )
200
+ objectClass ( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' DESC 'RFC2252: extensible object' SUP top AUXILIARY )
201
+ objectClass ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
202
+ EOS
203
+ end
204
+
205
+ # After loading object classes and attr types: resolve oid strings to point
206
+ # to objects. This will expose schema inconsistencies (e.g. objectclass
207
+ # has unknown SUP class or points to unknown attributeType). However,
208
+ # unknown Syntaxes just create new Syntax objects.
209
+
210
+ def resolve_oids
211
+
212
+ all_attrtypes.each do |a|
213
+ if a.sup
214
+ s = find_attrtype(a.sup)
215
+ a.instance_eval {
216
+ @sup = s
217
+ # inherit properties (FIXME: This breaks to_def)
218
+ @equality ||= s.equality
219
+ @ordering ||= s.ordering
220
+ @substr ||= s.substr
221
+ @syntax ||= s.syntax
222
+ @maxlen ||= s.maxlen
223
+ @singlevalue ||= s.singlevalue
224
+ @collective ||= s.collective
225
+ @nousermod ||= s.nousermod
226
+ @usage ||= s.usage
227
+ }
228
+ end
229
+ a.instance_eval do
230
+ @syntax = LDAP::Server::Syntax.find(@syntax) if @syntax
231
+ @equality = LDAP::Server::MatchingRule.find(@equality) if @equality
232
+ @ordering = LDAP::Server::MatchingRule.find(@ordering) if @ordering
233
+ @substr = LDAP::Server::MatchingRule.find(@substr) if @substr
234
+ end
235
+ end
236
+
237
+ all_objectclasses.each do |o|
238
+ if o.sup
239
+ s = o.sup.collect { |ss| find_objectclass(ss) }
240
+ o.instance_eval { @sup = s }
241
+ end
242
+ if o.must
243
+ s = o.must.collect { |ss| find_attrtype(ss) }
244
+ o.instance_eval { @must = s }
245
+ end
246
+ if o.may
247
+ s = o.may.collect { |ss| find_attrtype(ss) }
248
+ o.instance_eval { @may = s }
249
+ end
250
+ end
251
+
252
+ end
253
+
254
+ # Validate a new entry or update. For a new entry, just pass a hash
255
+ # of attr=>[val, val, ...]; for an update, the first parameter is
256
+ # a hash of attr=>[:modtype, val, val...] and the second parameter
257
+ # is the existing entry, where it is assumed that the attribute names
258
+ # are already in their standard string forms (as returned by attr#name)
259
+ #
260
+ # Returns a hash containing the updated entry.
261
+ #
262
+ # If a block is given, it is called to decide whether the user is
263
+ # allowed to update an attribute; parameter is the attr *object*
264
+ # (not name; use #name if you need its name instead). Return false
265
+ # if the update is not permitted. Otherwise, the only restriction
266
+ # will be that updates to attributes declared 'nousermod' are forbidden.
267
+ #
268
+ # No DN checks are done here, since we don't know the DN.
269
+ # Checking that the entry contains an attribute for the RDN is the
270
+ # responsibility of the caller.
271
+
272
+ def validate(mods, entry={})
273
+
274
+ # Run through the mods, make the normalized names, and perform any
275
+ # updates
276
+
277
+ # FIXME: I don't know if these are the right results to return
278
+ # for the various types of validation errors
279
+
280
+ oc_changed = false
281
+ res = entry.dup
282
+ mods.each do |attrname, nv|
283
+ attr = find_attrtype(attrname)
284
+ attrname = attr.to_s
285
+ raise LDAP::ResultError::ConstraintViolation,
286
+ "Cannot modify #{attrname}" if attr.nousermod or
287
+ (block_given? and !yield(attr))
288
+ # Perform the update
289
+ vals = res[attrname] || []
290
+ checkvals = []
291
+ nv = [nv] unless nv.is_a?(Array)
292
+
293
+ case nv.first
294
+ when :add
295
+ checkvals = nv[1..-1]
296
+ vals += checkvals
297
+ vals.uniq! # FIXME: ?? error if duplicate values
298
+ # FIXME: normalize values? e.g. c: gb and c: GB are same value.
299
+ when :delete
300
+ nv = nv[1..-1]
301
+ if nv.empty?
302
+ vals = [] # ?? error if does not exist
303
+ else
304
+ nv.each { |v| vals.delete(v) } # ?? error if value missing
305
+ end
306
+ when :replace
307
+ vals = checkvals = nv[1..-1]
308
+ else
309
+ vals = checkvals = nv
310
+ end
311
+ if vals == []
312
+ res.delete(attrname)
313
+ else
314
+ res[attrname] = vals
315
+ end
316
+
317
+ # Attribute validation
318
+ raise LDAP::ResultError::ObjectClassViolation,
319
+ "Attribute #{attr} is SINGLE-VALUE" if attr.singlevalue and vals.size > 1
320
+
321
+ checkvals.each do |val|
322
+ raise LDAP::ResultError::InvalidAttributeSyntax,
323
+ "Nil or empty value for attribute #{attr}" if val.nil? or val.empty?
324
+ raise LDAP::ResultError::InvalidAttributeSyntax,
325
+ "Bad value for #{attr}: #{val.inspect}" if attr.syntax and ! attr.syntax.match(val)
326
+ raise LDAP::ResultError::InvalidAttributeSyntax,
327
+ "Value too long for #{attr} (max #{attr.maxlen})" if attr.maxlen and val.length > attr.maxlen
328
+ end
329
+
330
+ oc_changed = true if attrname == 'objectClass'
331
+ end
332
+
333
+ # Now do objectClass checks
334
+ oc = res['objectClass']
335
+ unless oc
336
+ raise LDAP::ResultError::ObjectClassViolation,
337
+ "objectClass attribute missing"
338
+ end
339
+ oc = oc.collect { |val| find_objectclass(val) }
340
+
341
+ if oc_changed
342
+ # Add superior objectClasses (note: growing an array while you
343
+ # iterate over it seems to work, in ruby-1.8.2 anyway!)
344
+ oc.each do |objectclass|
345
+ objectclass.sup.each do |s|
346
+ oc.push(s) unless oc.include?(s)
347
+ end
348
+ end
349
+ res['objectClass'] = oc.collect { |oo| oo.to_s }
350
+
351
+ # Check that exactly one structural objectClass is present
352
+ unless oc.find_all { |s| s.struct == :structural }.size >= 1
353
+ raise LDAP::ResultError::ObjectClassViolation,
354
+ "Entry must have at least one structural objectClass"
355
+ # Exactly one? But you have to sort out the inheritance problem
356
+ # (e.g. both person and organizationalPerson are declared
357
+ # structural)
358
+ end
359
+ end
360
+
361
+ # Ensure that all MUST attributes are present
362
+ allow_attr = {}
363
+ oc.each do |objectclass|
364
+ objectclass.must.each do |m|
365
+ unless res[m.name] and res[m.name] != []
366
+ raise LDAP::ResultError::ObjectClassViolation, "Missing attribute #{m} required by objectClass #{objectclass}"
367
+ end
368
+ allow_attr[m.name] = true
369
+ end
370
+ objectclass.may.each do |m|
371
+ allow_attr[m.name] = true
372
+ end
373
+ end
374
+
375
+ unless oc.find { |objectclass| objectclass.name == 'extensibleObject' }
376
+ # Now check all the attributes given are permitted by MUST or MAY
377
+ res.each_key do |attr|
378
+ unless allow_attr[attr] or find_attrtype(attr).usage == :directoryOperation
379
+ raise LDAP::ResultError::ObjectClassViolation, "Attribute #{attr} not permitted by objectClass"
380
+ end
381
+ end
382
+ end
383
+
384
+ return res
385
+ end
386
+
387
+ # Hopefully backwards-compatible API for ruby-ldap's LDAP::Schema.
388
+ # Since MUST/MAY/SUP may point to schema objects, convert them back
389
+ # to strings.
390
+
391
+ def names(key)
392
+ case key
393
+ when 'objectClasses'
394
+ return all_objectclasses.collect { |e| e.name }
395
+ when 'attributeTypes'
396
+ return all_attrtypes.collect { |e| e.name }
397
+ when 'ldapSyntaxes'
398
+ return LDAP::Server::Syntax.all_syntaxes.collect { |e| e.name }
399
+ when 'matchingRules'
400
+ return LDAP::Server::MatchingRule.all_matching_rules.collect { |e| e.name }
401
+ # TODO: matchingRuleUse
402
+ end
403
+ return nil
404
+ end
405
+
406
+ # Backwards-compatible for ruby-ldap LDAP::Schema
407
+
408
+ def attr(oc,at)
409
+ o = find_objectclass(oc)
410
+ case at.upcase
411
+ when 'MUST'
412
+ return o.must.collect { |e| e.to_s }
413
+ when 'MAY'
414
+ return o.may.collect { |e| e.to_s }
415
+ when 'SUP'
416
+ return o.sup.collect { |e| e.to_s }
417
+ when 'NAME'
418
+ return o.names.collect { |e| e.to_s }
419
+ when 'DESC'
420
+ return [o.desc]
421
+ end
422
+ return nil
423
+ rescue LDAP::ResultError
424
+ return nil
425
+ end
426
+
427
+ # Backwards-compatible for ruby-ldap LDAP::Schema
428
+
429
+ def must(oc)
430
+ attr(oc, "MUST")
431
+ end
432
+
433
+ # Backwards-compatible for ruby-ldap LDAP::Schema
434
+
435
+ def may(oc)
436
+ attr(oc, "MAY")
437
+ end
438
+
439
+ # Backwards-compatible for ruby-ldap LDAP::Schema
440
+
441
+ def sup(oc)
442
+ attr(oc, "SUP")
443
+ end
444
+
445
+ #####################################################################
446
+
447
+ # Class holding an instance of an AttributeTypeDescription (RFC2252 4.2)
448
+
449
+ class AttributeType
450
+
451
+ attr_reader :oid, :names, :desc, :obsolete, :sup, :equality, :ordering
452
+ attr_reader :substr, :syntax, :maxlen, :singlevalue, :collective
453
+ attr_reader :nousermod, :usage
454
+
455
+ def initialize(str)
456
+ m = LDAP::Server::Syntax::AttributeTypeDescription.match(str)
457
+ raise LDAP::ResultError::InvalidAttributeSyntax,
458
+ "Bad AttributeTypeDescription #{str.inspect}" unless m
459
+ @oid = m[1]
460
+ @names = (m[2]||"").scan(/'(.*?)'/).flatten
461
+ @desc = m[3]
462
+ @obsolete = ! m[4].nil?
463
+ @sup = m[5]
464
+ @equality = m[6]
465
+ @ordering = m[7]
466
+ @substr = m[8]
467
+ @syntax = m[9]
468
+ @maxlen = m[10] && m[10].to_i
469
+ @singlevalue = ! m[11].nil?
470
+ @collective = ! m[12].nil?
471
+ @nousermod = ! m[13].nil?
472
+ @usage = m[14] && m[14].intern
473
+ # This is the cache of the stringified version. Rather than
474
+ # initialize to str, we set nil to force it to be rebuilt
475
+ @def = nil
476
+ end
477
+
478
+ def name
479
+ @names.first
480
+ end
481
+
482
+ def to_s
483
+ (@names && @names.first) || @oid
484
+ end
485
+
486
+ def changed
487
+ @def = nil
488
+ end
489
+
490
+ def to_def
491
+ return @def if @def
492
+ ans = "( #{@oid} "
493
+ if @names.nil? or @names.empty?
494
+ # nothing
495
+ elsif @names.size == 1
496
+ ans << "NAME '#{@names.first}' "
497
+ else
498
+ ans << "NAME ( "
499
+ @names.each { |n| ans << "'#{n}' " }
500
+ ans << ") "
501
+ end
502
+ ans << "DESC '#{@desc}' " if @desc
503
+ ans << "OBSOLETE " if @obsolete
504
+ ans << "SUP #{@sup} " if @sup # oid
505
+ ans << "EQUALITY #{@equality} " if @equality # oid
506
+ ans << "ORDERING #{@ordering} " if @ordering # oid
507
+ ans << "SUBSTR #{@substr} " if @substr # oid
508
+ ans << "SYNTAX #{@syntax}#{@maxlen && "{#{@maxlen}}"} " if @syntax
509
+ ans << "SINGLE-VALUE " if @singlevalue
510
+ ans << "COLLECTIVE " if @collective
511
+ ans << "NO-USER-MODIFICATION " if @nousermod
512
+ ans << "USAGE #{@usage} " if @usage
513
+ ans << ")"
514
+ @def = ans
515
+ end
516
+ end # class AttributeType
517
+
518
+ #####################################################################
519
+
520
+ # Class holding an instance of an ObjectClassDescription (RFC2252 4.4)
521
+
522
+ class ObjectClass
523
+
524
+ attr_reader :oid, :names, :desc, :obsolete, :sup, :struct, :must, :may
525
+
526
+ SCAN_WOID = /#{LDAP::Server::Syntax::WOID}/x
527
+
528
+ def initialize(str)
529
+ m = LDAP::Server::Syntax::ObjectClassDescription.match(str)
530
+ raise LDAP::ResultError::InvalidAttributeSyntax,
531
+ "Bad ObjectClassDescription #{str.inspect}" unless m
532
+ @oid = m[1]
533
+ @names = (m[2]||"").scan(/'(.*?)'/).flatten
534
+ @desc = m[3]
535
+ @obsolete = ! m[4].nil?
536
+ @sup = (m[5]||"").scan(SCAN_WOID).flatten
537
+ @struct = m[6] ? m[6].downcase.intern : :structural
538
+ @must = (m[7]||"").scan(SCAN_WOID).flatten
539
+ @may = (m[8]||"").scan(SCAN_WOID).flatten
540
+ @def = nil
541
+ end
542
+
543
+ def name
544
+ @names.first
545
+ end
546
+
547
+ def to_s
548
+ (@names && @names.first) || @oid
549
+ end
550
+
551
+ def changed
552
+ @def = nil
553
+ end
554
+
555
+ def to_def
556
+ return @def if @def
557
+ ans = "( #{@oid} "
558
+ if @names.nil? or @names.empty?
559
+ # nothing
560
+ elsif @names.size == 1
561
+ ans << "NAME '#{@names.first}' "
562
+ else
563
+ ans << "NAME ( "
564
+ @names.each { |n| ans << "'#{n}' " }
565
+ ans << ") "
566
+ end
567
+ ans << "DESC '#{@desc}' " if @desc
568
+ ans << "OBSOLETE " if @obsolete
569
+ ans << joinoids("SUP ",@sup," ")
570
+ ans << "#{@struct.to_s.upcase} " if @struct
571
+ ans << joinoids("MUST ",@must," ")
572
+ ans << joinoids("MAY ",@may," ")
573
+ ans << ")"
574
+ @def = ans
575
+ end
576
+
577
+ def joinoids(pfx,arr,sfx)
578
+ return "" unless arr and !arr.empty?
579
+ return "#{pfx}#{arr}#{sfx}" unless arr.is_a?(Array)
580
+ a = arr.collect { |elem| elem.to_s }
581
+ if a.size == 1
582
+ return "#{pfx}#{a.first}#{sfx}"
583
+ else
584
+ return "#{pfx}( #{a.join(" $ ")} )#{sfx}"
585
+ end
586
+ end
587
+ end # class ObjectClass
588
+
589
+ end # class Schema
590
+
591
+ end # class Server
592
+ end # module LDAP