ldaptic 0.2.0

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.
Files changed (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +104 -0
  3. data/Rakefile +41 -0
  4. data/lib/ldaptic.rb +151 -0
  5. data/lib/ldaptic/active_model.rb +37 -0
  6. data/lib/ldaptic/adapters.rb +90 -0
  7. data/lib/ldaptic/adapters/abstract_adapter.rb +123 -0
  8. data/lib/ldaptic/adapters/active_directory_adapter.rb +78 -0
  9. data/lib/ldaptic/adapters/active_directory_ext.rb +12 -0
  10. data/lib/ldaptic/adapters/ldap_conn_adapter.rb +262 -0
  11. data/lib/ldaptic/adapters/net_ldap_adapter.rb +173 -0
  12. data/lib/ldaptic/adapters/net_ldap_ext.rb +24 -0
  13. data/lib/ldaptic/attribute_set.rb +283 -0
  14. data/lib/ldaptic/dn.rb +365 -0
  15. data/lib/ldaptic/entry.rb +646 -0
  16. data/lib/ldaptic/error_set.rb +34 -0
  17. data/lib/ldaptic/errors.rb +136 -0
  18. data/lib/ldaptic/escape.rb +110 -0
  19. data/lib/ldaptic/filter.rb +282 -0
  20. data/lib/ldaptic/methods.rb +387 -0
  21. data/lib/ldaptic/railtie.rb +9 -0
  22. data/lib/ldaptic/schema.rb +246 -0
  23. data/lib/ldaptic/syntaxes.rb +319 -0
  24. data/test/core.schema +582 -0
  25. data/test/ldaptic_active_model_test.rb +40 -0
  26. data/test/ldaptic_adapters_test.rb +35 -0
  27. data/test/ldaptic_attribute_set_test.rb +57 -0
  28. data/test/ldaptic_dn_test.rb +110 -0
  29. data/test/ldaptic_entry_test.rb +22 -0
  30. data/test/ldaptic_errors_test.rb +23 -0
  31. data/test/ldaptic_escape_test.rb +47 -0
  32. data/test/ldaptic_filter_test.rb +53 -0
  33. data/test/ldaptic_hierarchy_test.rb +90 -0
  34. data/test/ldaptic_schema_test.rb +44 -0
  35. data/test/ldaptic_syntaxes_test.rb +66 -0
  36. data/test/mock_adapter.rb +47 -0
  37. data/test/rbslapd1.rb +111 -0
  38. data/test/rbslapd4.rb +172 -0
  39. data/test/test_helper.rb +2 -0
  40. metadata +146 -0
@@ -0,0 +1,44 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)),'test_helper')
2
+ require 'ldaptic/schema'
3
+
4
+ class LdapticSyntaxesTest < Test::Unit::TestCase
5
+ NAME_FORM = "(1.2.3 NAME 'foo' DESC ('bar') OC objectClass MUST (cn $ ou) X-AWESOME TRUE)"
6
+ ATTRIBUTE_TYPE = "(1.3.5 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15{256}')"
7
+ def assert_parse_error(&block)
8
+ assert_raise(Ldaptic::Schema::ParseError, &block)
9
+ end
10
+
11
+ def test_name_form
12
+ name_form = Ldaptic::Schema::NameForm.new(NAME_FORM)
13
+ assert_equal "1.2.3", name_form.oid
14
+ assert_equal "foo", name_form.name
15
+ assert_equal %w(foo), name_form.names
16
+ assert_equal %w(cn ou), name_form.must
17
+ assert name_form.x_awesome?
18
+ assert !name_form.x_lame
19
+ assert_raise(NoMethodError) { name_form.applies }
20
+ assert_raise(ArgumentError) { name_form.desc(1) }
21
+ assert_raise(ArgumentError) { name_form.x_lame(1) }
22
+ assert_equal nil, name_form.may
23
+ assert_equal NAME_FORM, name_form.to_s
24
+ assert name_form.inspect.include?("#<Ldaptic::Schema::NameForm")
25
+ assert name_form.inspect.include?("1.2.3")
26
+ end
27
+
28
+ def test_object_class
29
+ assert_equal "AUXILIARY", Ldaptic::Schema::ObjectClass.new("(1.2 AUXILIARY)").kind
30
+ end
31
+
32
+ def test_attribute_type
33
+ attribute_type = Ldaptic::Schema::AttributeType.new(ATTRIBUTE_TYPE)
34
+ assert_equal 256, attribute_type.syntax_len
35
+ assert_not_nil attribute_type.syntax
36
+ end
37
+
38
+ def test_parse_error
39
+ assert_parse_error { Ldaptic::Schema::NameForm.new("x") }
40
+ assert_parse_error { Ldaptic::Schema::NameForm.new("(1.2.3 NAME (foo | bar))") }
41
+ assert_parse_error { Ldaptic::Schema::NameForm.new("(1.2.3 &)") }
42
+ end
43
+
44
+ end
@@ -0,0 +1,66 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)),'test_helper')
2
+ require 'ldaptic/syntaxes'
3
+
4
+ class LdapticSyntaxesTest < Test::Unit::TestCase
5
+
6
+ def test_for
7
+ assert_equal Ldaptic::Syntaxes::GeneralizedTime, Ldaptic::Syntaxes.for("Generalized Time")
8
+ end
9
+
10
+ def test_bit_string
11
+ assert_nil Ldaptic::Syntaxes::BitString.new.error("'01'B")
12
+ assert_not_nil Ldaptic::Syntaxes::BitString.new.error("01'B")
13
+ end
14
+
15
+ def test_boolean
16
+ assert_equal true, Ldaptic::Syntaxes::Boolean.parse("TRUE")
17
+ assert_equal false, Ldaptic::Syntaxes::Boolean.parse("FALSE")
18
+ assert_equal "TRUE", Ldaptic::Syntaxes::Boolean.format(true)
19
+ assert_equal "FALSE", Ldaptic::Syntaxes::Boolean.format(false)
20
+ end
21
+
22
+ def test_postal_address
23
+ assert_not_nil Ldaptic::Syntaxes::PostalAddress.new.error('\\a')
24
+ end
25
+
26
+ def test_generalized_time
27
+ assert_equal Time.utc(2000,1,1,12,34,56), Ldaptic::Syntaxes::GeneralizedTime.parse("20000101123456.0Z")
28
+ assert_equal Time.utc(2000,1,1,12,34,56), Ldaptic::Syntaxes::GeneralizedTime.parse("20000101123456.0Z")
29
+ assert_equal 1601, Ldaptic::Syntaxes::GeneralizedTime.parse("16010101000001.0Z").year
30
+ assert_equal "20000101123456.000000Z", Ldaptic::Syntaxes::GeneralizedTime.format(Time.utc(2000,1,1,12,34,56))
31
+ end
32
+
33
+ def test_ia5_string
34
+ assert_nil Ldaptic::Syntaxes::IA5String.new.error('a')
35
+ end
36
+
37
+ def test_integer
38
+ assert_equal 1, Ldaptic::Syntaxes::INTEGER.parse("1")
39
+ assert_equal "1", Ldaptic::Syntaxes::INTEGER.format(1)
40
+ end
41
+
42
+ def test_printable_string
43
+ assert_nil Ldaptic::Syntaxes::PrintableString.new.error("Az0'\"()+,-./:? =")
44
+ assert_not_nil Ldaptic::Syntaxes::PrintableString.new('$')
45
+ assert_not_nil Ldaptic::Syntaxes::PrintableString.new("\\")
46
+ assert_not_nil Ldaptic::Syntaxes::PrintableString.new("\t")
47
+ end
48
+
49
+ def test_country_string
50
+ assert_nil Ldaptic::Syntaxes::CountryString.new.error('ab')
51
+ assert_not_nil Ldaptic::Syntaxes::CountryString.new.error('a')
52
+ assert_not_nil Ldaptic::Syntaxes::CountryString.new.error('abc')
53
+ assert_not_nil Ldaptic::Syntaxes::CountryString.new.error('a_')
54
+ end
55
+
56
+ def test_delivery_method
57
+ assert_not_nil Ldaptic::Syntaxes::DeliveryMethod.new.error('')
58
+ end
59
+
60
+ def test_facsimile_telephone_number
61
+ assert_nil Ldaptic::Syntaxes::FacsimileTelephoneNumber.new.error("911")
62
+ assert_nil Ldaptic::Syntaxes::FacsimileTelephoneNumber.new.error("911$b4Length")
63
+ assert_not_nil Ldaptic::Syntaxes::FacsimileTelephoneNumber.new("\t")
64
+ end
65
+
66
+ end
@@ -0,0 +1,47 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__),'..','lib')).uniq!
2
+ require 'ldaptic/adapters'
3
+ require 'ldaptic/adapters/abstract_adapter'
4
+
5
+ class Ldaptic::Adapters::MockAdapter < Ldaptic::Adapters::AbstractAdapter
6
+ register_as(:mock)
7
+
8
+ def schema(arg = nil)
9
+ {
10
+ 'objectClasses' => [
11
+ "( 2.5.6.0 NAME 'top' ABSTRACT MUST (objectClass) MAY (cn $ description $ distinguishedName) )",
12
+ "( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST (cn) MAY (sn $ age) )",
13
+ "( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' SUP top AUXILIARY MAY userPassword )",
14
+ "( 9.9.9.1 NAME 'searchResult' SUP top STRUCTURAL MUST (filter $ scope) )"
15
+ ],
16
+ 'attributeTypes' => [
17
+ "( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
18
+ "( 2.5.4.3 NAME ( 'cn' 'commonName' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
19
+ "( 2.5.4.49 NAME 'distinguishedName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE NO-USER-MODIFICATION )",
20
+ "( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
21
+ "( 2.5.4.4 NAME ( 'sn' 'surname' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
22
+ "( 2.5.4.35 NAME 'userPassword' SYNTAX '1.3.6.1.4.1.1466.115.121.1.40' )",
23
+ "( 2.5.4.4 NAME 'filter' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
24
+ "( 9.9.9.2 NAME 'scope' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
25
+ "( 9.9.9.2 NAME 'age' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
26
+ ],
27
+ "dITContentRules" => [
28
+ "( 2.5.6.6 NAME 'person' AUX simpleSecurityObject )"
29
+ ]
30
+ }
31
+ end
32
+
33
+ def server_default_base_dn
34
+ "DC=org"
35
+ end
36
+
37
+ # Returns a mock object which encapsulates the search query.
38
+ def search(options)
39
+ yield({
40
+ 'objectClass' => %w(top searchResult),
41
+ 'filter' => [options[:filter].to_s],
42
+ 'scope' => [options[:scope].to_s],
43
+ 'dn' => [options[:base]]
44
+ })
45
+ 0
46
+ end
47
+ end
data/test/rbslapd1.rb ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # This is a trivial LDAP server which just stores directory entries in RAM.
4
+ # It does no validation or authentication. This is intended just to
5
+ # demonstrate the API, it's not for real-world use!!
6
+
7
+ $:.unshift('../lib')
8
+ $debug = true
9
+
10
+ require 'ldap/server'
11
+
12
+ # We subclass the Operation class, overriding the methods to do what we need
13
+
14
+ class HashOperation < LDAP::Server::Operation
15
+ def initialize(connection, messageID, hash)
16
+ super(connection, messageID)
17
+ @hash = hash # an object reference to our directory data
18
+ end
19
+
20
+ def search(basedn, scope, deref, filter)
21
+ basedn.downcase!
22
+
23
+ case scope
24
+ when LDAP::Server::BaseObject
25
+ # client asked for single object by DN
26
+ obj = @hash[basedn]
27
+ raise LDAP::ResultError::NoSuchObject unless obj
28
+ send_SearchResultEntry(basedn, obj) if LDAP::Server::Filter.run(filter, obj)
29
+
30
+ when LDAP::Server::WholeSubtree
31
+ @hash.each do |dn, av|
32
+ next unless dn.index(basedn, -basedn.length) # under basedn?
33
+ next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
34
+ send_SearchResultEntry(dn, av)
35
+ end
36
+
37
+ else
38
+ raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
39
+
40
+ end
41
+ end
42
+
43
+ def add(dn, av)
44
+ dn.downcase!
45
+ raise LDAP::ResultError::EntryAlreadyExists if @hash[dn]
46
+ @hash[dn] = av
47
+ end
48
+
49
+ def del(dn)
50
+ dn.downcase!
51
+ raise LDAP::ResultError::NoSuchObject unless @hash.has_key?(dn)
52
+ @hash.delete(dn)
53
+ end
54
+
55
+ def modify(dn, ops)
56
+ entry = @hash[dn]
57
+ raise LDAP::ResultError::NoSuchObject unless entry
58
+ ops.each do |attr, vals|
59
+ op = vals.shift
60
+ case op
61
+ when :add
62
+ entry[attr] ||= []
63
+ entry[attr] += vals
64
+ entry[attr].uniq!
65
+ when :delete
66
+ if vals == []
67
+ entry.delete(attr)
68
+ else
69
+ vals.each { |v| entry[attr].delete(v) }
70
+ end
71
+ when :replace
72
+ entry[attr] = vals
73
+ end
74
+ entry.delete(attr) if entry[attr] == []
75
+ end
76
+ end
77
+ end
78
+
79
+ # This is the shared object which carries our actual directory entries.
80
+ # It's just a hash of {dn=>entry}, where each entry is {attr=>[val,val,...]}
81
+
82
+ directory = {}
83
+
84
+ # Let's put some backing store on it
85
+
86
+ require 'yaml'
87
+ begin
88
+ File.open("ldapdb.yaml") { |f| directory = YAML::load(f.read) }
89
+ rescue Errno::ENOENT
90
+ end
91
+
92
+ at_exit do
93
+ File.open("ldapdb.new","w") { |f| f.write(YAML::dump(directory)) }
94
+ File.rename("ldapdb.new","ldapdb.yaml")
95
+ end
96
+
97
+ # Listen for incoming LDAP connections. For each one, create a Connection
98
+ # object, which will invoke a HashOperation object for each request.
99
+
100
+ s = LDAP::Server.new(
101
+ :port => 1389,
102
+ :nodelay => true,
103
+ :listen => 10,
104
+ # :ssl_key_file => "key.pem",
105
+ # :ssl_cert_file => "cert.pem",
106
+ # :ssl_on_connect => true,
107
+ :operation_class => HashOperation,
108
+ :operation_args => [directory]
109
+ )
110
+ s.run_tcpserver
111
+ s.join
data/test/rbslapd4.rb ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # This is similar to rbslapd1.rb but here we use TOMITA Masahiro's prefork
4
+ # library: <http://raa.ruby-lang.org/project/prefork/>
5
+ # Advantages over Ruby threading:
6
+ # - each client connection is handled in its own process; don't need
7
+ # to worry about Ruby thread blocking (except if one client issues
8
+ # overlapping LDAP operations down the same connection, which is uncommon)
9
+ # - better scalability on multi-processor systems
10
+ # - better scalability on single-processor systems (e.g. shouldn't hit
11
+ # max FDs per process limit)
12
+ # Disadvantages:
13
+ # - client connections can't share state in RAM. So our shared directory
14
+ # now has to be read from disk, and flushed to disk after every update.
15
+ #
16
+ # Additionally, I have added schema support. An LDAP v3 client can
17
+ # query the schema remotely, and adds/modifies have data validated.
18
+
19
+ $:.unshift('../lib')
20
+
21
+ require 'ldap/server'
22
+ require 'ldap/server/schema'
23
+ require 'yaml'
24
+
25
+ $debug = nil # $stderr
26
+
27
+ # An object to keep our in-RAM database and synchronise it to disk
28
+ # when necessary
29
+
30
+ class Directory
31
+ attr_reader :data
32
+
33
+ def initialize(filename)
34
+ @filename = filename
35
+ @stat = nil
36
+ update
37
+ end
38
+
39
+ # synchronise with directory on disk (re-read if it has changed)
40
+
41
+ def update
42
+ begin
43
+ tmp = {}
44
+ sb = File.stat(@filename)
45
+ return if @stat and @stat.ino == sb.ino and @stat.mtime == sb.mtime
46
+ File.open(@filename) do |f|
47
+ tmp = YAML::load(f.read)
48
+ @stat = f.stat
49
+ end
50
+ rescue Errno::ENOENT
51
+ end
52
+ @data = tmp
53
+ end
54
+
55
+ # write back to disk
56
+
57
+ def write
58
+ File.open(@filename+".new","w") { |f| f.write(YAML::dump(@data)) }
59
+ File.rename(@filename+".new",@filename)
60
+ @stat = File.stat(@filename)
61
+ end
62
+
63
+ # run a block while holding a lock on the database
64
+
65
+ def lock
66
+ File.open(@filename+".lock","w") do |f|
67
+ f.flock(File::LOCK_EX) # will block here until lock available
68
+ yield
69
+ end
70
+ end
71
+ end
72
+
73
+ # We subclass the Operation class, overriding the methods to do what we need
74
+
75
+ class DirOperation < LDAP::Server::Operation
76
+ def initialize(connection, messageID, dir)
77
+ super(connection, messageID)
78
+ @dir = dir
79
+ end
80
+
81
+ def search(basedn, scope, deref, filter)
82
+ $debug << "Search: basedn=#{basedn.inspect}, scope=#{scope.inspect}, deref=#{deref.inspect}, filter=#{filter.inspect}\n" if $debug
83
+ basedn.downcase!
84
+
85
+ case scope
86
+ when LDAP::Server::BaseObject
87
+ # client asked for single object by DN
88
+ @dir.update
89
+ obj = @dir.data[basedn]
90
+ raise LDAP::ResultError::NoSuchObject unless obj
91
+ ok = LDAP::Server::Filter.run(filter, obj)
92
+ $debug << "Match=#{ok.inspect}: #{obj.inspect}\n" if $debug
93
+ send_SearchResultEntry(basedn, obj) if ok
94
+
95
+ when LDAP::Server::WholeSubtree
96
+ @dir.update
97
+ @dir.data.each do |dn, av|
98
+ $debug << "Considering #{dn}\n" if $debug
99
+ next unless dn.index(basedn, -basedn.length) # under basedn?
100
+ next unless LDAP::Server::Filter.run(filter, av) # attribute filter?
101
+ $debug << "Sending: #{av.inspect}\n" if $debug
102
+ send_SearchResultEntry(dn, av)
103
+ end
104
+
105
+ else
106
+ raise LDAP::ResultError::UnwillingToPerform, "OneLevel not implemented"
107
+
108
+ end
109
+ end
110
+
111
+ def add(dn, entry)
112
+ entry = @schema.validate(entry)
113
+ entry['createTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
114
+ entry['creatorsName'] = [@connection.binddn.to_s]
115
+ # FIXME: normalize the DN and check it's below our root DN
116
+ # FIXME: validate that a superior object exists
117
+ # FIXME: validate that entry contains the RDN attribute (yuk)
118
+ dn.downcase!
119
+ @dir.lock do
120
+ @dir.update
121
+ raise LDAP::ResultError::EntryAlreadyExists if @dir.data[dn]
122
+ @dir.data[dn] = entry
123
+ @dir.write
124
+ end
125
+ end
126
+
127
+ def del(dn)
128
+ dn.downcase!
129
+ @dir.lock do
130
+ @dir.update
131
+ raise LDAP::ResultError::NoSuchObject unless @dir.data.has_key?(dn)
132
+ @dir.data.delete(dn)
133
+ @dir.write
134
+ end
135
+ end
136
+
137
+ def modify(dn, ops)
138
+ dn.downcase!
139
+ @dir.lock do
140
+ @dir.update
141
+ entry = @dir.data[dn]
142
+ raise LDAP::ResultError::NoSuchObject unless entry
143
+ entry = @schema.validate(ops, entry) # also does the update
144
+ entry['modifyTimestamp'] = [Time.now.gmtime.strftime("%Y%m%d%H%MZ")]
145
+ entry['modifiersName'] = [@connection.binddn.to_s]
146
+ @dir.data[dn] = entry
147
+ @dir.write
148
+ end
149
+ end
150
+ end
151
+
152
+ directory = Directory.new("ldapdb.yaml")
153
+
154
+ schema = LDAP::Server::Schema.new
155
+ schema.load_system
156
+ schema.load_file("../test/core.schema")
157
+ schema.resolve_oids
158
+
159
+ s = LDAP::Server.new(
160
+ :port => 1389,
161
+ :nodelay => true,
162
+ :listen => 10,
163
+ # :ssl_key_file => "key.pem",
164
+ # :ssl_cert_file => "cert.pem",
165
+ # :ssl_on_connect => true,
166
+ :operation_class => DirOperation,
167
+ :operation_args => [directory],
168
+ :schema => schema,
169
+ :namingContexts => ['dc=example,dc=com']
170
+ )
171
+ s.run_tcpserver
172
+ s.join
@@ -0,0 +1,2 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__),'..','lib')).uniq!
2
+ require 'test/unit'