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
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Tim Pope
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,104 @@
1
+ = Ldaptic
2
+
3
+ This is an object-oriented LDAP wrapper library I started back in 2007 but
4
+ only recently polished up and released. It's unique in that it creates a
5
+ class hierarchy (in a namespace your provide) that exactly mirrors the class
6
+ hierarchy on the server. For example, on a typical server, you'll get an
7
+ +InetOrgPerson+ class which inherits from +OrganizationalPerson+ which
8
+ inherits from +Person+ which inherits from +Top+. You can reopen any of these
9
+ classes to add additional client side behavior.
10
+
11
+ Ldaptic started as mainly a tool to interact with my company's Active
12
+ Directory server, and I lost interest in it when I left that job. Recently,
13
+ I've become motivated to work on it again, as some of the blocking issues I
14
+ faced are now potentially solvable with Active Model.
15
+
16
+ == Getting Started
17
+
18
+ You need to have either the ruby-ldap or net-ldap gem installed. The former
19
+ is preferred because it's faster native C. Ldaptic is configured by including
20
+ a dynamically created module into a namespace of your choosing.
21
+
22
+ module Example
23
+ include Ldaptic::Module(
24
+ :adapter => :ldap_conn,
25
+ :base => 'ou=Users,dc=example,dc=com',
26
+ :host => 'example.com',
27
+ :username => 'cn=admin,ou=Users,dc=example,dc=com',
28
+ :password => 'password'
29
+ )
30
+ end
31
+
32
+ The adapter field can usually be omitted as it defaults to :ldap_conn or
33
+ :net_ldap, based on which of the above two gems can be found (though you might
34
+ want to use the :active_directory adapter, which depends on ruby-ldap,
35
+ instead). If the base is omitted, it will use the first naming context on the
36
+ server (usually what you want).
37
+
38
+ Entries are retrieved using the search method. Named parameters include
39
+ :base, :filter, :sort, :limit, :scope, and :attributes. All are optional.
40
+
41
+ entries = Example.search(
42
+ :filter => {:objectClass => 'inetOrgPerson'}
43
+ :sort => :cn,
44
+ :limit => 10
45
+ )
46
+
47
+ A Ruby class is created for each objectClass defined on the server. Entries
48
+ are instances of these classes.
49
+
50
+ >> entry = Example.find('cn=admin,ou=Users,dc=example,dc=com')
51
+ => #<Example::InetOrgPerson cn=admin,ou=Users,dc=example,dc=com ...>
52
+ >> entry.class.superclass
53
+ => Example::OrganizationalPerson
54
+
55
+ Predictably, entries have attribute readers and writers.
56
+
57
+ >> entry.cn
58
+ => <["admin"]>
59
+ >> entry.cn = "root"
60
+ >> entry[:cn]
61
+ => <["root"]>
62
+ >> entry[:cn] = "admin"
63
+
64
+ The returned object is an attribute set and behaves similar to an array. Some
65
+ attributes are marked by the server as "single value;" those will return the
66
+ first element on method access but an attribute set on indexing access, for
67
+ programmatic convenience.
68
+
69
+ >> entry.uidNumber
70
+ => 0
71
+ >> entry[:uidNumber]
72
+ => <[0]>
73
+
74
+ The indexing syntax can also be used to create and fetch children.
75
+
76
+ >> users
77
+ => #<Example::OrganizationalUnit ou=Users,dc=example,dc=com ...>
78
+ >> users[:cn=>'admin'] = Example::InetOrgPerson.new
79
+ => #<Example::InetOrgPerson cn=admin,ou=Users,dc=example,dc=com ...>
80
+ >> users[:cn=>'admin']
81
+ => #<Example::InetOrgPerson cn=admin,ou=Users,dc=example,dc=com ...>
82
+
83
+ Entries also implement many of the standard methods you've come to expect in
84
+ an Active Record world (save, valid?, errors, to_param, attributes, ...).
85
+ In fact, it is fully Active Model compliant.
86
+
87
+ For more information, see in particular Ldaptic::Methods (for namespace
88
+ methods like search), Ldaptic::Entry, and Ldaptic::AttributeSet.
89
+
90
+ == To Do
91
+
92
+ * Verify everything still works. The tests take care of much of this, but
93
+ there are no integration tests for the adapters. In particular, I have no
94
+ way to verify the Active Directory adapter still works.
95
+
96
+ * The test suite (reflecting my fledgling testing abilities from 2007) is more
97
+ smoke test than BDD. Perhaps switch to RSpec in the quest to rectify this.
98
+
99
+ * Potential new features (mostly along the lines of "make it more like Active
100
+ Record") are in the GitHub issue tracker. Vote for and comment on the ones
101
+ you would find useful, as most are on hold until someone has a real use
102
+ case.
103
+
104
+ * Pick a better name? Ldaptic has an ambiguous spelling.
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue LoadError
4
+ end
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+ require 'rake/packagetask'
8
+ require 'rake/gempackagetask'
9
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
10
+
11
+ task :default => :test
12
+
13
+ Rake::TestTask.new do |t|
14
+ t.libs << "test"
15
+ t.test_files = Dir['test/*_test.rb']
16
+ t.verbose = true
17
+ end
18
+
19
+ Rake::RDocTask.new do |rdoc|
20
+ rdoc.rdoc_dir = 'doc'
21
+ rdoc.rdoc_files.add('README.rdoc', 'lib')
22
+ rdoc.main = 'README.rdoc'
23
+ rdoc.title = 'Ldaptic'
24
+ rdoc.options << '--inline-source'
25
+ rdoc.options << '-d' if `which dot` =~ /\/dot/
26
+ end
27
+
28
+ spec = eval(File.read(File.join(File.dirname(__FILE__), 'ldaptic.gemspec')))
29
+ Rake::GemPackageTask.new(spec) do |p|
30
+ p.gem_spec = spec
31
+ end
32
+
33
+ begin
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |t|
36
+ t.test_files = Dir['test/*_test.rb']
37
+ t.verbose = true
38
+ t.rcov_opts << "--exclude '/(rcov|net-ldap|i18n|active(?:support|model))-'"
39
+ end
40
+ rescue LoadError
41
+ end
data/lib/ldaptic.rb ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ldaptic/dn'
4
+ require 'ldaptic/filter'
5
+ require 'ldaptic/errors'
6
+ require 'ldaptic/schema'
7
+ require 'ldaptic/syntaxes'
8
+ require 'ldaptic/adapters'
9
+ require 'ldaptic/entry'
10
+ require 'ldaptic/methods'
11
+
12
+ # = Getting started
13
+ #
14
+ # See the methods of the Ldaptic module (below) for information on connecting.
15
+ #
16
+ # See the Ldaptic::Methods module for information on searching with your
17
+ # connection object.
18
+ #
19
+ # Search results are Ldaptic::Entry objects. See the documentation for this
20
+ # class for information on manipulating and updating them, as well as creating
21
+ # new entries.
22
+ module Ldaptic
23
+
24
+ SCOPES = {
25
+ :base => 0, # ::LDAP::LDAP_SCOPE_BASE,
26
+ :onelevel => 1, # ::LDAP::LDAP_SCOPE_ONELEVEL,
27
+ :subtree => 2 # ::LDAP::LDAP_SCOPE_SUBTREE
28
+ }
29
+
30
+ # Default logger. If none given, creates a new logger on $stderr.
31
+ def self.logger
32
+ unless @logger
33
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
34
+ return Rails.logger
35
+ else
36
+ require 'logger'
37
+ @logger = Logger.new($stderr)
38
+ @logger.level = Logger::WARN
39
+ end
40
+ end
41
+ @logger
42
+ end
43
+
44
+ def self.logger=(logger)
45
+ @logger = logger
46
+ end
47
+
48
+ # Returns an object that can be assigned directly to a variable. This allows
49
+ # for an "anonymous" Ldaptic object.
50
+ # @my_company = Ldaptic::Object(options)
51
+ # @my_company::User.class_eval do
52
+ # alias login sAMAccountName
53
+ # end
54
+ def self.Object(options={}, &block)
55
+ base = ::Module.new do
56
+ include Ldaptic::Module(options)
57
+ end
58
+ if block_given?
59
+ base.class_eval(&block)
60
+ end
61
+ base
62
+ end
63
+
64
+ # Similar to Ldaptic::Class, accepting the same options. Instead of
65
+ # returning an anonymous class that activates upon inheritance, it returns an
66
+ # anonymous module that activates upon inclusion.
67
+ # module MyCompany
68
+ # include Ldaptic::Module(options)
69
+ # # This class and many others are created automatically based on
70
+ # # information from the server.
71
+ # class User
72
+ # alias login sAMAccountName
73
+ # end
74
+ # end
75
+ #
76
+ # me = MyCompany.search(:filter => {:cn => "Name, My"}).first
77
+ # puts me.login
78
+ def self.Module(options={})
79
+ Ldaptic::Module.new(options)
80
+ end
81
+
82
+ # The core constructor of Ldaptic. This method returns an anonymous class
83
+ # which can then be inherited from.
84
+ #
85
+ # The new class is not intended to be instantiated, instead serving as a
86
+ # namespace. Included in this namespace is a set of class methods, as found
87
+ # in Ldaptic::Methods, and a class hierarchy mirroring the object classes
88
+ # found on the server.
89
+ #
90
+ # options = {
91
+ # :adapter => :active_directory,
92
+ # :host => "pdc.mycompany.com",
93
+ # :username => "mylogin@mycompany.com",
94
+ # :password => "mypassword"
95
+ # }
96
+ #
97
+ # class MyCompany < Ldaptic::Class(options)
98
+ # # This class and many others are created automatically based on
99
+ # # information from the server.
100
+ # class User
101
+ # alias login sAMAccountName
102
+ # end
103
+ # end
104
+ #
105
+ # me = MyCompany.search(:filter => {:cn => "Name, My"}).first
106
+ # puts me.login
107
+ #
108
+ # Options given to this method are relayed to Ldaptic::Adapters.for. The
109
+ # documentation for this method should be consulted for further information.
110
+ def self.Class(options={})
111
+ klass = ::Class.new(Class)
112
+ klass.instance_variable_set(:@options, Ldaptic::Adapters.for(options))
113
+ klass
114
+ end
115
+
116
+ class << self
117
+ alias Namespace Class
118
+ end
119
+
120
+ # An instance of this subclass of ::Module is returned by the Ldaptic::Module
121
+ # method.
122
+ class Module < ::Module #:nodoc:
123
+ def initialize(options={})
124
+ super()
125
+ @options = options
126
+ end
127
+ def append_features(base)
128
+ base.extend(Methods)
129
+ base.instance_variable_set(:@adapter, Ldaptic::Adapters.for(@options))
130
+ base.module_eval { build_hierarchy }
131
+ end
132
+ end
133
+
134
+ # The anonymous class returned by the Ldaptic::Class method descends from
135
+ # this class.
136
+ class Class #:nodoc:
137
+ class << self
138
+ # Callback which triggers the magic.
139
+ def inherited(subclass)
140
+ if options = @options
141
+ subclass.class_eval { include Ldaptic::Module.new(options) }
142
+ else
143
+ subclass.instance_variable_set(:@adapter, @adapter)
144
+ end
145
+ super
146
+ end
147
+ private :inherited, :new
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_model'
2
+ require 'ldaptic'
3
+
4
+ ActiveModel::EachValidator.class_eval do
5
+ def validate(record)
6
+ attributes.each do |attribute|
7
+ values = record.read_attribute_for_validation(attribute)
8
+ values = [values] unless values.respond_to?(:before_type_cast)
9
+ values.each do |value|
10
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
11
+ validate_each(record, attribute, value)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ class Ldaptic::Entry
18
+ include ActiveModel::Validations
19
+ include ActiveModel::Serializers::Xml
20
+ include ActiveModel::Serializers::JSON
21
+ include ActiveModel::Dirty
22
+ include ActiveModel::Callbacks
23
+
24
+ def read_attribute_for_validation(attribute)
25
+ read_attribute(attribute.to_sym, true)
26
+ end
27
+
28
+ # define_model_callbacks(:save, :destroy)
29
+
30
+ validate do
31
+ @attributes.keys.each do |key|
32
+ self[key].errors.each do |error|
33
+ errors.add(key, error)
34
+ end
35
+ end
36
+ end if respond_to?(:validate)
37
+ end
@@ -0,0 +1,90 @@
1
+ module Ldaptic
2
+ # RFC1823 - The LDAP Application Program Interface
3
+ module Adapters
4
+
5
+ @adapters ||= {}
6
+
7
+ class <<self
8
+
9
+ # Internally used by adapters to make themselves available.
10
+ def register(name, mod) #:nodoc:
11
+ @adapters[name.to_sym] = mod
12
+ @adapters
13
+ end
14
+
15
+ # Returns a new adapter for a given set of options. This method is not
16
+ # for end user use but is instead called by the Ldaptic::Class,
17
+ # Ldaptic::Module, and Ldaptic::Object methods.
18
+ #
19
+ # The <tt>:adapter</tt> key of the +options+ hash selects which adapter
20
+ # to use. The following adapters are included with Ldaptic.
21
+ #
22
+ # * <tt>:ldap_conn</tt>: a Ruby/LDAP LDAP::Conn connection.
23
+ # * <tt>:ldap_sslconn</tt>: a Ruby/LDAP LDAP::SSLConn connection.
24
+ # * <tt>:active_directory</tt>: A wrapper around Ruby/LDAP which takes
25
+ # into account some of the idiosyncrasies of Active Directory.
26
+ # * <tt>:net_ldap</tt>: a net-ldap Net::LDAP connection.
27
+ #
28
+ # All other options given are passed directly to the adapter. While
29
+ # different adapters support different options, the following are
30
+ # typically supported.
31
+ #
32
+ # * <tt>:host</tt>: The host to connect to. The default is localhost.
33
+ # * <tt>:port</tt>: The TCP port to use. Default is provided by the
34
+ # underlying connection.
35
+ # * <tt>:username</tt>: The DN to bind with. If not given, an anonymous
36
+ # bind is used.
37
+ # * <tt>:password</tt>: Password for binding.
38
+ # * <tt>:base</tt>: The default base DN. Derived from the server by
39
+ # default.
40
+ def for(options)
41
+ require 'ldaptic/adapters/abstract_adapter'
42
+ # Allow an adapter to be passed directly in for backwards compatibility.
43
+ if defined?(::LDAP::Conn) && options.kind_of?(::LDAP::Conn)
44
+ options = {:adapter => :ldap_conn, :connection => options}
45
+ elsif defined?(::Net::LDAP) && options.kind_of?(::Net::LDAP)
46
+ options = {:adapter => :net_ldap, :connection => options}
47
+ end
48
+ if options.kind_of?(Hash)
49
+ options = options.inject({}) {|h,(k,v)| h[k.to_sym] = v; h}
50
+ if options.has_key?(:connection) && !options.has_key?(:adapter)
51
+ options[:adapter] = options[:connection].class.name.downcase.gsub('::','_')
52
+ end
53
+ options[:adapter] ||= default_adapter
54
+ unless options[:adapter]
55
+ Ldaptic::Errors.raise(ArgumentError.new("No adapter specfied"))
56
+ end
57
+ begin
58
+ require "ldaptic/adapters/#{options[:adapter]}_adapter"
59
+ rescue LoadError
60
+ end
61
+ if adapter = @adapters[options[:adapter].to_sym]
62
+ adapter.new(options)
63
+ else
64
+ Ldaptic::Errors.raise(ArgumentError.new("Adapter #{options[:adapter]} not found"))
65
+ end
66
+ else
67
+ if options.kind_of?(::Ldaptic::Adapters::AbstractAdapter)
68
+ options
69
+ else
70
+ Ldaptic::Errors.raise(TypeError.new("#{options.class} is not a valid connection type"))
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+ def default_adapter
77
+ require 'ldap'
78
+ :ldap_conn
79
+ rescue LoadError
80
+ begin
81
+ require 'net/ldap'
82
+ :net_ldap
83
+ rescue LoadError
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end