fakeldap 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +3 -0
- data/lib/fakeldap.rb +46 -0
- data/lib/fakeldap/version.rb +4 -0
- data/vendor/ruby-ldapserver/COPYING +27 -0
- data/vendor/ruby-ldapserver/ChangeLog +83 -0
- data/vendor/ruby-ldapserver/Manifest.txt +32 -0
- data/vendor/ruby-ldapserver/README +222 -0
- data/vendor/ruby-ldapserver/Rakefile +22 -0
- data/vendor/ruby-ldapserver/examples/README +89 -0
- data/vendor/ruby-ldapserver/examples/mkcert.rb +31 -0
- data/vendor/ruby-ldapserver/examples/rbslapd1.rb +111 -0
- data/vendor/ruby-ldapserver/examples/rbslapd2.rb +161 -0
- data/vendor/ruby-ldapserver/examples/rbslapd3.rb +172 -0
- data/vendor/ruby-ldapserver/examples/speedtest.rb +37 -0
- data/vendor/ruby-ldapserver/lib/ldap/server.rb +4 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/connection.rb +276 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/filter.rb +223 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/match.rb +283 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/operation.rb +487 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/preforkserver.rb +93 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/result.rb +71 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/schema.rb +592 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/server.rb +89 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/syntax.rb +235 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/tcpserver.rb +91 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/util.rb +88 -0
- data/vendor/ruby-ldapserver/lib/ldap/server/version.rb +11 -0
- data/vendor/ruby-ldapserver/test/core.schema +582 -0
- data/vendor/ruby-ldapserver/test/encoding_test.rb +279 -0
- data/vendor/ruby-ldapserver/test/filter_test.rb +107 -0
- data/vendor/ruby-ldapserver/test/match_test.rb +59 -0
- data/vendor/ruby-ldapserver/test/schema_test.rb +113 -0
- data/vendor/ruby-ldapserver/test/syntax_test.rb +40 -0
- data/vendor/ruby-ldapserver/test/test_helper.rb +2 -0
- data/vendor/ruby-ldapserver/test/util_test.rb +51 -0
- 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
|