fakeldap 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +3 -0
  3. data/lib/fakeldap.rb +46 -0
  4. data/lib/fakeldap/version.rb +4 -0
  5. data/vendor/ruby-ldapserver/COPYING +27 -0
  6. data/vendor/ruby-ldapserver/ChangeLog +83 -0
  7. data/vendor/ruby-ldapserver/Manifest.txt +32 -0
  8. data/vendor/ruby-ldapserver/README +222 -0
  9. data/vendor/ruby-ldapserver/Rakefile +22 -0
  10. data/vendor/ruby-ldapserver/examples/README +89 -0
  11. data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
  12. data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
  13. data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
  14. data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
  15. data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
  16. data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
  17. data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
  18. data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
  19. data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
  20. data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
  21. data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
  22. data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
  23. data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
  24. data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
  25. data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
  26. data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
  27. data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
  28. data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
  29. data/vendor/ruby-ldapserver/test/core.schema +582 -0
  30. data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
  31. data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
  32. data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
  33. data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
  34. data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
  35. data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
  36. data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
  37. metadata +130 -0
@@ -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