ldaptic 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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