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.
- data/COPYING +27 -0
- data/ChangeLog +83 -0
- data/Manifest.txt +32 -0
- data/README +222 -0
- data/Rakefile +22 -0
- data/examples/README +89 -0
- data/examples/mkcert.rb +31 -0
- data/examples/rbslapd1.rb +111 -0
- data/examples/rbslapd2.rb +161 -0
- data/examples/rbslapd3.rb +172 -0
- data/examples/speedtest.rb +37 -0
- data/lib/ldap/server.rb +4 -0
- data/lib/ldap/server/connection.rb +273 -0
- data/lib/ldap/server/filter.rb +223 -0
- data/lib/ldap/server/match.rb +283 -0
- data/lib/ldap/server/operation.rb +487 -0
- data/lib/ldap/server/preforkserver.rb +93 -0
- data/lib/ldap/server/result.rb +71 -0
- data/lib/ldap/server/schema.rb +592 -0
- data/lib/ldap/server/server.rb +89 -0
- data/lib/ldap/server/syntax.rb +235 -0
- data/lib/ldap/server/tcpserver.rb +91 -0
- data/lib/ldap/server/util.rb +88 -0
- data/lib/ldap/server/version.rb +11 -0
- data/test/core.schema +582 -0
- data/test/encoding_test.rb +279 -0
- data/test/filter_test.rb +107 -0
- data/test/match_test.rb +59 -0
- data/test/schema_test.rb +113 -0
- data/test/syntax_test.rb +40 -0
- data/test/test_helper.rb +2 -0
- data/test/util_test.rb +51 -0
- metadata +98 -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
|