activedirectory 0.9.3

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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+
2
+ The ActiveDirectory Module for Ruby is licensed under the MIT license (below).
3
+
4
+ ------------------------------------------------------------------------------
5
+
6
+ Copyright (c) 2005-2006 Justin Mecham
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to
10
+ deal in the Software without restriction, including without limitation the
11
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
+ sell copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
+ IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,139 @@
1
+ = Active Directory Module for Ruby
2
+
3
+ This module enables effortless interaction with Active Directory servers.
4
+
5
+ It is built atop the Ruby/LDAP[http://ruby-ldap.sourceforge.net/] extension
6
+ with explicit support for the idiosyncrasies of Microsoft Windows Servers and
7
+ the LDAP objects defined therein. This includes support for:
8
+
9
+ * Users (objectClass=user)
10
+ * Groups (objectClass=group)
11
+ * Additional objects will be supported in future releases (e.g. Computers,
12
+ Printers, etc)
13
+
14
+ == Installation
15
+
16
+ === RubyGems
17
+
18
+ The preferred method if installing the Active Directory Module for Ruby is
19
+ with RubyGems[http://docs.rubygems.org/]. Installing with RubyGems is as
20
+ simple as typing:
21
+
22
+ gem install activedirectory-x.x.x.gem
23
+
24
+ If you did not download the gem file and instead have the source distribution
25
+ extracted you may create a gem by typing:
26
+
27
+ rake package
28
+
29
+ in the directory containing the Active Directory Module for Ruby source.
30
+
31
+ === Source
32
+
33
+ If you prefer not to use RubyGems to install the library, you may install the
34
+ module from source by typing:
35
+
36
+ ruby install.rb
37
+
38
+ in the directory containing the Active Directory Module for Ruby source.
39
+
40
+ == Usage Instructions
41
+
42
+ === Including the module
43
+
44
+ Once installed, you must include the ActiveDirectory module into your project.
45
+ If you are using RubyGems, use:
46
+
47
+ require_gem "activedirectory"
48
+
49
+ Otherwise, include the module with:
50
+
51
+ require "activedirectory"
52
+
53
+ === Configuring your Active Directory instance
54
+
55
+ Interacting with an Active Directory instance requires that you first
56
+ configure the connection to a Windows domain controller. This configuration
57
+ needs to be called only once when your application starts up. If you are using
58
+ this module within a Ruby on Rails[http://www.rubyonrails.org/] application,
59
+ you would place this in your environment.rb file.
60
+
61
+ ActiveDirectory::Base.server_settings = {
62
+ :host => "server.example.com",
63
+ :username => "username",
64
+ :password => "password",
65
+ :domain => "example.com",
66
+ :base_dn => "DC=example,DC=com"
67
+ }
68
+
69
+ Once ActiveDirectory::Base has been configured, any call into the API will
70
+ open a new connection to the server on-demand (or use an existing open
71
+ connection).
72
+
73
+ == Example Usage
74
+
75
+ === User Objects
76
+
77
+ Locating users within a directory is very flexible. You may load individual
78
+ users by their account name (sAMAccountName), their distinguished name (dn),
79
+ or any number of users based on the criteria used in your LDAP search base and
80
+ filters.
81
+
82
+ # Load all users (including disabled) within the default Base DN.
83
+ all_users = ActiveDirectory::User.find(:all)
84
+
85
+ # Load all disabled users within the default Base DN.
86
+ disabled_users = ActiveDirectory::User.find(:all,
87
+ :filter => "(userAccountControl=514)")
88
+
89
+ # Load all users who are in the Managers organizational unit whose accounts
90
+ # are not disabled.
91
+ managers = ActiveDirectory::User.find(:all,
92
+ :base => "OU=Managers,DC=example,DC=com",
93
+ :filter => "(userAccountControl=512)")
94
+
95
+ # Load the user "John Doe" by his sAMAccountName.
96
+ user = ActiveDirectory::User.find("jdoe")
97
+
98
+ # Load the user "John Doe" by his distinguished name (DN).
99
+ user = ActiveDirectory::User.find("CN=John Doe,CN=Users,DC=example,DC=com")
100
+
101
+ === Group Objects
102
+
103
+ Locating Groups is just as simple as Users, however, you must provide a
104
+ Distinguished Name (DN) for loading.
105
+
106
+ # Load all groups within the default Base DN.
107
+ all_groups = ActiveDirectory::Group.find(:all)
108
+
109
+ # Load the "Developers" group by its distinguished name (DN).
110
+ developers = ActiveDirectory::Group.find("CN=Developers,DC=example,DC=com")
111
+
112
+ == License
113
+
114
+ The Active Directory Module for Ruby module is licensed under the MIT License.
115
+
116
+ Copyright (c) 2005-2006 Justin Mecham
117
+
118
+ Permission is hereby granted, free of charge, to any person obtaining a copy
119
+ of this software and associated documentation files (the "Software"), to
120
+ deal in the Software without restriction, including without limitation the
121
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
122
+ sell copies of the Software, and to permit persons to whom the Software is
123
+ furnished to do so, subject to the following conditions:
124
+
125
+ The above copyright notice and this permission notice shall be included in
126
+ all copies or substantial portions of the Software.
127
+
128
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
129
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
130
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
131
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
132
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
133
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
134
+ IN THE SOFTWARE.
135
+
136
+ == Contact
137
+
138
+ The Active Directory Module for Ruby is developed and maintained by Justin
139
+ Mecham (jmecham@justin@aspect.net[mailto:jmecham@justin@aspect.net]).
data/Rakefile ADDED
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require File.join(File.dirname(__FILE__), 'lib', 'active_directory', 'version')
8
+
9
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
10
+ PKG_NAME = 'activedirectory'
11
+ PKG_VERSION = ActiveDirectory::VERSION::STRING + PKG_BUILD
12
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
13
+
14
+ RELEASE_NAME = "REL #{PKG_VERSION}"
15
+
16
+ desc "Default Task"
17
+ task :default => [ :test ]
18
+
19
+
20
+ # Run the unit tests
21
+ Rake::TestTask.new { |t|
22
+ t.libs << "test"
23
+ t.pattern = 'test/*_test.rb'
24
+ t.verbose = true
25
+ }
26
+
27
+
28
+ # Genereate the RDoc documentation
29
+ Rake::RDocTask.new { |rdoc|
30
+ rdoc.rdoc_dir = 'doc'
31
+ rdoc.title = "Active Directory Module for Ruby -- Effortless Interaction with Active Directory"
32
+ rdoc.options << "--line-numbers" << "--inline-source"
33
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
34
+ rdoc.rdoc_files.include(['README', 'LICENSE'])
35
+ rdoc.rdoc_files.include('lib/**/*.rb')
36
+ }
37
+
38
+ # Create compressed packages
39
+ spec = Gem::Specification.new do |s|
40
+ s.platform = Gem::Platform::RUBY
41
+ s.name = PKG_NAME
42
+ s.summary = "Module for easy interaction with Active Directory servers."
43
+ s.description = %q{Makes it trivial to integrate with Active Directory servers for information and authentication concerning users and groups.}
44
+ s.version = PKG_VERSION
45
+
46
+ s.author = "Justin Mecham"
47
+ s.email = "justin@aspect.net"
48
+
49
+ s.has_rdoc = true
50
+ s.requirements << 'none'
51
+ s.require_path = 'lib'
52
+ s.autorequire = 'active_directory'
53
+
54
+ s.files = [ "Rakefile", "install.rb", "README", "LICENSE" ]
55
+ s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
56
+ s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
57
+ end
58
+
59
+ Rake::GemPackageTask.new(spec) do |pkg|
60
+ pkg.gem_spec = spec
61
+ pkg.need_tar = true
62
+ pkg.need_zip = true
63
+ end
64
+
65
+
66
+ #desc "Publish the API documentation"
67
+ #task :pgem => [:package] do
68
+ # Rake::SshFilePublisher.new("[user@host]", "[path]", "pkg", "#{PKG_FILE_NAME}.gem").upload
69
+ #end
70
+
71
+
72
+ #desc "Publish the API documentation"
73
+ #task :pdoc => [:rdoc] do
74
+ # Rake::SshDirPublisher.new("[user@host]", "[path]", "doc").upload
75
+ #end
76
+
data/install.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ $sitedir = CONFIG["sitelibdir"]
8
+ unless $sitedir
9
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
10
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
11
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
12
+ if !$sitedir
13
+ $sitedir = File.join($libdir, "site_ruby")
14
+ elsif $sitedir !~ Regexp.quote(version)
15
+ $sitedir = File.join($sitedir, version)
16
+ end
17
+ end
18
+
19
+ Dir.chdir("lib")
20
+
21
+ Find.find("active_directory", "active_directory.rb") { |f|
22
+ if f[-3..-1] == ".rb"
23
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
24
+ else
25
+ File::makedirs(File.join($sitedir, *f.split(/\//)))
26
+ end
27
+ }
@@ -0,0 +1,15 @@
1
+ $:.unshift(File.dirname(__FILE__) + "/active_directory/vendor/")
2
+
3
+ # External Dependencies
4
+ require 'ldap'
5
+ require 'logger'
6
+
7
+ # Extensions to Ruby
8
+ Dir[File.dirname(__FILE__) + "/active_directory/ext/*.rb"].each { |file|
9
+ require file
10
+ }
11
+
12
+ # Active Directory Classes
13
+ require 'active_directory/base'
14
+ require 'active_directory/user'
15
+ require 'active_directory/group'
@@ -0,0 +1,368 @@
1
+ #--
2
+ # Active Directory Module for Ruby
3
+ #
4
+ # Copyright (c) 2005-2006 Justin Mecham
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to
8
+ # deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ # sell copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ # IN THE SOFTWARE.
23
+ #++
24
+
25
+ module ActiveDirectory
26
+
27
+ #
28
+ # The ActiveDirectory module contains the classes used for communicating with
29
+ # Active Directory LDAP servers. ActiveDirectory::Base is the basis from which
30
+ # all classes derive.
31
+ #
32
+ class Base
33
+
34
+ #
35
+ # Configuration
36
+ #
37
+ @@server_settings = {
38
+ :host => "localhost",
39
+ :port => 389,
40
+ :username => nil,
41
+ :password => nil,
42
+ :domain => nil,
43
+ :base_dn => nil
44
+ }
45
+ cattr_accessor :server_settings
46
+
47
+ #
48
+ # Logging
49
+ #
50
+ cattr_writer :logger
51
+ def self.logger
52
+ @@logger || Logger.new(STDOUT)
53
+ end
54
+
55
+ #
56
+ # Returns a connection to the configured Active Directory instance. If a
57
+ # connection is not already established, it will attempt to establish the
58
+ # connection.
59
+ #
60
+ def self.connection
61
+ @@connection ||= connect
62
+ end
63
+
64
+ #
65
+ # Opens and returns a connection to the Active Directory instance. By
66
+ # default, secure connections will be attempted first while gracefully
67
+ # falling back to lesser secure methods.
68
+ #
69
+ # The order by which connections are attempted are: TLS, SSL, unencrypted.
70
+ #
71
+ # Calling #connection will automatically call this method if a connection
72
+ # has not already been established.
73
+ #
74
+ def self.connect
75
+
76
+ host = @@server_settings[:host]
77
+ port = @@server_settings[:port] || 389
78
+
79
+ # Attempt to connect using TLS
80
+ begin
81
+ connection = LDAP::SSLConn.new(host, port, true)
82
+ connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
83
+ bind(connection)
84
+ logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} using TLS...") unless logger.nil?
85
+ rescue
86
+ logger.debug("ActiveDirectory: Failed to connect to #{@@server_settings[:host]} using TLS!") unless logger.nil?
87
+ # Attempt to connect using SSL
88
+ begin
89
+ connection = LDAP::SSLConn.new(host, port, false)
90
+ connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
91
+ bind(connection)
92
+ logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} over SSL...") unless logger.nil?
93
+ rescue
94
+ logger.debug("ActiveDirectory: Failed to connect to #{@@server_settings[:host]} over SSL!") unless logger.nil?
95
+ # Attempt to connect without encryption
96
+ begin
97
+ connection = LDAP::Conn.new(host, port)
98
+ connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
99
+ bind(connection)
100
+ logger.info("ActiveDirectory: Connected to #{@@server_settings[:host]} without encryption!") unless logger.nil?
101
+ rescue
102
+ logger.error("ActiveDirectory: Failed to connect to #{@@server_settings[:host]}!") unless logger.nil?
103
+ puts "EXCEPTION: #{$!}"
104
+ connection = nil
105
+ raise
106
+ end
107
+ end
108
+ end
109
+
110
+ connection.set_option(LDAP::LDAP_OPT_REFERRALS, 0)
111
+
112
+ connection
113
+
114
+ end
115
+
116
+ #
117
+ # Unbinds (if bound) and closes the current connection.
118
+ #
119
+ def self.close
120
+ begin
121
+ @@connection.unbind unless @@connection.nil?
122
+ rescue
123
+ end
124
+ @@connection = nil
125
+ end
126
+
127
+ #
128
+ # Attempts to reconnect to the server by closing the existing connection
129
+ # and reconnecting.
130
+ #
131
+ def self.reconnect
132
+ close
133
+ @@connection = connect
134
+ end
135
+
136
+ #
137
+ # Search interface for querying for objects within the directory, such as
138
+ # users and groups. Searching the directory is quite similar to a normal
139
+ # LDAP search, but with a better API.
140
+ #
141
+ # If you call this method from User or Group, it will narrow your searches
142
+ # to those specific objects by default. Calling this method on Base will
143
+ # return objects of any class and provides the most flexibility.
144
+ #
145
+ # ==== Searching Users
146
+ #
147
+ # Users may be located within the directory by their distinguished name
148
+ # (DN), their username (sAMAccountName), or through any other valid LDAP
149
+ # filter and optional search base.
150
+ #
151
+ # # Load all users (including disabled) within the default Base DN.
152
+ # all_users = ActiveDirectory::User.find(:all)
153
+ #
154
+ # # Load all disabled users within the default Base DN.
155
+ # disabled_users = ActiveDirectory::User.find(:all,
156
+ # :filter => "(userAccountControl=514)")
157
+ #
158
+ # # Load all users who are in the Managers organizational unit whose
159
+ # # accounts are not disabled.
160
+ # managers = ActiveDirectory::User.find(:all,
161
+ # :base => "OU=Managers,DC=example,DC=com",
162
+ # :filter => "(userAccountControl=512)")
163
+ #
164
+ # # Load the user "John Doe" by his sAMAccountName.
165
+ # user = ActiveDirectory::User.find("jdoe")
166
+ #
167
+ # # Load the user "John Doe" by his distinguished name (DN).
168
+ # user = ActiveDirectory::User.find("CN=John Doe,CN=Users,DC=example,DC=com")
169
+ #
170
+ # ==== Searching Groups
171
+ #
172
+ # Groups may be located within the diretctory by their distinguished name
173
+ # (DN) or through any other valid LDAP filter and optional search base.
174
+ #
175
+ # # Load all groups within the default Base DN.
176
+ # all_groups = ActiveDirectory::Group.find(:all)
177
+ #
178
+ # # Load the "Developers" group by its distinguished name (DN).
179
+ # developers = ActiveDirectory::Group.find("CN=Developers,DC=example,DC=com")
180
+ #
181
+ # ==== More Advanced Examples
182
+ #
183
+ # By calling ActiveDirectory::Base#find you can query objects across
184
+ # classes, allowing you to pull in both Groups and Users that match your
185
+ # criteria.
186
+ #
187
+ # # Load all Contacts
188
+ # contacts = ActiveDirectory::Base.find(:all,
189
+ # :filter => "(&(objectClass=User)(objectCategory=Contact))")
190
+ #
191
+ def self.find(*args)
192
+
193
+ options = extract_options_from_args!(args)
194
+ attributes = [ "distinguishedName", "objectClass" ]
195
+
196
+ # Determine the appropriate search filter
197
+ if self.name == "ActiveDirectory::Base"
198
+ if options[:filter]
199
+ filter = options[:filter]
200
+ else
201
+ filter = "(|(&(objectCategory=Person)(objectClass=User))(objectClass=Group))"
202
+ end
203
+ else
204
+ subklass = class_name_of_active_directory_descendent(self)
205
+ if subklass == "ActiveDirectory::User"
206
+ filter = "(&(objectCategory=Person)(objectClass=User)#{options[:filter]})"
207
+ elsif subklass == "ActiveDirectory::Group"
208
+ filter = "(&(objectClass=Group)#{options[:filter]})"
209
+ end
210
+ end
211
+
212
+ # Determine the appropriate search base
213
+ base_dn = options[:base] ? options[:base] : @@server_settings[:base_dn]
214
+
215
+ # Determine the appropriate scope
216
+ scope = options[:scope] ? options[:scope] : LDAP::LDAP_SCOPE_SUBTREE
217
+
218
+ # Load all matching objects
219
+ if args.first == :all
220
+
221
+ logger.debug "Searching Active Directory" \
222
+ " - Filter: #{filter}" \
223
+ " - Search Base: #{base_dn} " \
224
+ " - Scope: #{scope}"
225
+
226
+ # Perform search
227
+ entries = self.search(base_dn, scope, filter, attributes)
228
+
229
+ result = Array.new
230
+ unless entries.nil?
231
+ for entry in entries
232
+ if entry['objectClass'].include? "person"
233
+ result << User.new(entry['distinguishedName'][0])
234
+ elsif entry['objectClass'].include? "group"
235
+ result << Group.new(entry['distinguishedName'][0])
236
+ end
237
+ end
238
+ end
239
+ result
240
+
241
+ else
242
+
243
+ # Load a single matching object by either a common name or a
244
+ # sAMAccountName
245
+ if args.first =~ /(CN|cn)=/
246
+ base_dn = args.first
247
+ else
248
+ filter = "(&(objectCategory=Person)(objectClass=User)(samAccountName=#{args.first})#{options[:filter]})"
249
+ end
250
+
251
+ logger.debug "Searching Active Directory" \
252
+ " - Filter: #{filter}" \
253
+ " - Search Base: #{base_dn} " \
254
+ " - Scope: #{scope}"
255
+
256
+ begin
257
+ entry = self.search(base_dn, scope, filter, attributes)
258
+ rescue
259
+ if $!.message == "No such object"
260
+ raise UnknownUserError
261
+ else
262
+ raise
263
+ end
264
+ end
265
+
266
+ entry = entry[0]
267
+ if entry['objectClass'].include? "person"
268
+ User.new(entry['distinguishedName'][0])
269
+ elsif entry['objectClass'].include? "group"
270
+ Group.new(entry['distinguishedName'][0])
271
+ end
272
+
273
+ end
274
+
275
+ end
276
+
277
+ protected
278
+
279
+ #
280
+ # Implement our own interface to LDAP::Conn#search so that we can attempt
281
+ # graceful reconnections when the connection becomes stale or otherwise
282
+ # terminates.
283
+ #
284
+ def self.search(base_dn, scope, filter, attrs = nil) #:nodoc:
285
+ retries = 0
286
+ entries = nil
287
+ begin
288
+ connection.search(base_dn, scope, filter, attrs) { |entry|
289
+ entries = Array.new if entries.nil?
290
+ entries << entry.to_hash
291
+ }
292
+ rescue
293
+ logger.debug("ActiveDirectory: Attempting to re-establish connection to Active Directory server... (Exception: \"#{$!}\" – Retry ##{retries} – #{$!.class})}") unless logger.nil?
294
+ begin
295
+ reconnect
296
+ rescue
297
+ (retries += 1) < 3 ? retry : raise
298
+ end
299
+ retry
300
+ end
301
+ entries
302
+ end
303
+
304
+ private
305
+
306
+ #
307
+ # Attempts to bind to the Active Directory instance. If a bind attempt
308
+ # fails by simple authentication an attempt at anonymous binding will be
309
+ # made.
310
+ #
311
+ def self.bind(connection)
312
+ # Attempt to bind with a username and password
313
+ begin
314
+ connection.bind("#{@@server_settings[:username]}@#{@@server_settings[:domain]}", @@server_settings[:password])
315
+ logger.info("ActiveDirectory: Bound to Active Directory server as #{@@server_settings[:username]}.") unless logger.nil?
316
+ rescue
317
+ logger.debug("ActiveDirectory: Failed to bind to Active Directory server as \"#{@@server_settings[:username]}\"!") unless logger.nil?
318
+ # Attempt to bind anonymously
319
+ begin
320
+ connection.bind()
321
+ logger.info("ActiveDirectory: Bound anonymously to Active Directory server.") unless logger.nil?
322
+ rescue
323
+ logger.debug("ActiveDirectory: Failed to bind anonymously to Active Directory server!") unless logger.nil?
324
+ raise
325
+ end
326
+ end
327
+ end
328
+
329
+ def self.extract_options_from_args!(args)
330
+ options = args.last.is_a?(Hash) ? args.pop : {}
331
+ validate_find_options(options)
332
+ options
333
+ end
334
+
335
+ def self.validate_find_options(options)
336
+ options.assert_valid_keys [ :base, :filter, :scope ]
337
+ end
338
+
339
+ def self.class_of_active_directory_descendent(klass)
340
+ if klass.superclass == Base
341
+ klass
342
+ elsif klass.superclass.nil?
343
+ raise ActiveDirectoryError, "#{name} doesn't belong in a class \
344
+ hierarchy descending from ActiveDirectory"
345
+ else
346
+ self.class_of_active_directory_descendent(klass.superclass)
347
+ end
348
+ end
349
+
350
+ def self.class_name_of_active_directory_descendent(klass)
351
+ self.class_of_active_directory_descendent(klass).name
352
+ end
353
+
354
+ end
355
+
356
+ class ActiveDirectoryError < StandardError #:nodoc:
357
+ end
358
+
359
+ class UnknownUserError < StandardError #:nodoc:
360
+ end
361
+
362
+ class UnknownGroupError < StandardError #:nodoc:
363
+ end
364
+
365
+ class PasswordInvalid < StandardError #:nodoc:
366
+ end
367
+
368
+ end
@@ -0,0 +1,50 @@
1
+ #
2
+ # Extends the class object with class and instance accessors for class
3
+ # attributes, just like the native attr* accessors for instance attributes.
4
+ #
5
+ # This was taken from ActiveSupport (see rubyonrails.org)
6
+ #
7
+ class Class # :nodoc:
8
+
9
+ def cattr_reader(*syms)
10
+ syms.flatten.each do |sym|
11
+ class_eval(<<-EOS, __FILE__, __LINE__)
12
+ unless defined? @@#{sym}
13
+ @@#{sym} = nil
14
+ end
15
+
16
+ def self.#{sym}
17
+ @@#{sym}
18
+ end
19
+
20
+ def #{sym}
21
+ @@#{sym}
22
+ end
23
+ EOS
24
+ end
25
+ end
26
+
27
+ def cattr_writer(*syms)
28
+ syms.flatten.each do |sym|
29
+ class_eval(<<-EOS, __FILE__, __LINE__)
30
+ unless defined? @@#{sym}
31
+ @@#{sym} = nil
32
+ end
33
+
34
+ def self.#{sym}=(obj)
35
+ @@#{sym} = obj
36
+ end
37
+
38
+ def #{sym}=(obj)
39
+ @@#{sym} = obj
40
+ end
41
+ EOS
42
+ end
43
+ end
44
+
45
+ def cattr_accessor(*syms)
46
+ cattr_reader(*syms)
47
+ cattr_writer(*syms)
48
+ end
49
+
50
+ end
@@ -0,0 +1,10 @@
1
+ #
2
+ # This was taken from ActiveSupport (see rubyonrails.org)
3
+ #
4
+ class Hash #:nodoc:
5
+ def assert_valid_keys(*valid_keys)
6
+ unknown_keys = keys - [valid_keys].flatten
7
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") \
8
+ unless unknown_keys.empty?
9
+ end
10
+ end
@@ -0,0 +1,109 @@
1
+ #--
2
+ # Active Directory Module for Ruby
3
+ #
4
+ # Copyright (c) 2005-2006 Justin Mecham
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to
8
+ # deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ # sell copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ # IN THE SOFTWARE.
23
+ #++
24
+
25
+ module ActiveDirectory
26
+
27
+ #
28
+ # Represents a Group object within an Active Directory instance.
29
+ #
30
+ class Group < Base
31
+
32
+ # Distinguished Name (DN)
33
+ attr_accessor :dn
34
+
35
+ # Description
36
+ attr_accessor :description
37
+
38
+ # Common Name (CN)
39
+ attr_accessor :name
40
+
41
+ # Users who are members of this Group
42
+ attr :members
43
+
44
+ #
45
+ # Attributes that we wish to pull from Active Directory for any Group that
46
+ # can be located within the directory.
47
+ #
48
+ ATTRIBUTES = ["distinguishedName", # DN of Group
49
+ "name", # Group Name
50
+ "member", # DN References to Members
51
+ "description"] #:nodoc: # Description
52
+
53
+ #
54
+ # Attempts to load a Group by a Distinguished Name (DN).
55
+ #
56
+ def initialize(identifier)
57
+
58
+ identifier = identifier.delete("\\")
59
+
60
+ load_by_dn(identifier)
61
+
62
+ end
63
+
64
+ #
65
+ # Proxy for loading and returning the members of this group.
66
+ #
67
+ def members #:nodoc:
68
+ members = Array.new
69
+ unless @members.nil?
70
+ for user_dn in @members
71
+ members << User.new(user_dn)
72
+ end
73
+ end
74
+ members
75
+ end
76
+
77
+ private
78
+
79
+ #
80
+ # Attempt to load a Group by its Distinguished Name (DN).
81
+ #
82
+ def load_by_dn(dn)
83
+
84
+ if dn.nil? or dn.length == 0
85
+ raise ArgumentError, "No distinguished name provided."
86
+ end
87
+
88
+ # De-escape the DN
89
+ dn = dn.gsub(/"/, "\\\"")
90
+
91
+ entries = Base.search(dn, LDAP::LDAP_SCOPE_BASE,
92
+ "(&(objectClass=group))", ATTRIBUTES)
93
+
94
+ raise UnknownGroupError if (entries.nil? or entries.length != 1)
95
+
96
+ parse_ldap_entry(entries[0])
97
+
98
+ end
99
+
100
+ def parse_ldap_entry(entry)
101
+ @dn = entry['distinguishedName'][0].delete("\\")
102
+ @name = entry['name'][0].delete("\\")
103
+ @description = entry['description'][0] unless entry['description'].nil?
104
+ @members = entry['member']
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,296 @@
1
+ #--
2
+ # Active Directory Module for Ruby
3
+ #
4
+ # Copyright (c) 2005-2006 Justin Mecham
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to
8
+ # deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ # sell copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ # IN THE SOFTWARE.
23
+ #++
24
+
25
+ module ActiveDirectory
26
+
27
+ #
28
+ # Represents a User object within an Active Directory instance.
29
+ #
30
+ class User < Base
31
+
32
+ # Distinguished Name (DN)
33
+ attr_reader :dn
34
+
35
+ # Display Name (e.g. "John Q. Public")
36
+ attr_reader :name
37
+
38
+ # Given Name (e.g. "John")
39
+ attr_reader :given_name
40
+
41
+ # Surname (e.g. "Public")
42
+ attr_reader :surname
43
+
44
+ # Primary E-Mail Address
45
+ attr_reader :email
46
+
47
+ # Account/Username (e.g. "jpublic")
48
+ attr_reader :username
49
+
50
+ # Job Title
51
+ attr_reader :title
52
+
53
+ # Company Name
54
+ attr_reader :company
55
+
56
+ # Department Name
57
+ attr_reader :department
58
+
59
+ # Primary Phone Number
60
+ attr_reader :main_number
61
+
62
+ # Mobile Number
63
+ attr_reader :mobile_number
64
+
65
+ # Street Address
66
+ attr_reader :street_address
67
+
68
+ # City / Town
69
+ attr_reader :city
70
+
71
+ # State/Province
72
+ attr_reader :state
73
+
74
+ # Zip/Postal Code
75
+ attr_reader :zip
76
+
77
+ # Country
78
+ attr_reader :country
79
+
80
+ # Direct Reports
81
+ attr :direct_reports
82
+
83
+ # Manager
84
+ attr :manager
85
+
86
+ # Groups this User is a member of
87
+ attr :groups
88
+
89
+ #
90
+ # Attributes that we wish to pull from Active Directory for any User that
91
+ # can be located within the directory.
92
+ #
93
+ ATTRIBUTES = ["displayName", # Name (e.g. "John Doe")
94
+ "givenName", # Given (First) Name
95
+ "sn", # Surname (Last)
96
+ "distinguishedName", # DN of User
97
+ "sAMAccountName", # Account Name
98
+ "mail", # Primary E-Mail Address
99
+ "manager", # DN Reference to Manager
100
+ "directReports", # DN References to Minions
101
+ "memberOf", # Group Membership
102
+ "company", # Company Name
103
+ "department", # Department Name
104
+ "title", # Title
105
+ "mobile", # Mobile Phone Number
106
+ "telephoneNumber", # Primary Phone Number
107
+ "streetAddress", # Street Address
108
+ "l", # City
109
+ "st", # State
110
+ "postalCode", # Zip Code
111
+ "co"] #:nodoc: # Country
112
+
113
+ #
114
+ # Attempts to load a User by a Distinguished Name (DN) or sAMAccountName.
115
+ #
116
+ def initialize(identifier)
117
+
118
+ if (identifier =~ /(CN|cn)=/) != nil
119
+ load_by_dn(identifier)
120
+ else
121
+ load_by_username(identifier)
122
+ end
123
+
124
+ end
125
+
126
+ #
127
+ # Attempts to authenticate the loaded user with the supplied password.
128
+ # Returns true if the authentication attempt was successful.
129
+ #
130
+ def authenticate(password)
131
+
132
+ # Clean up the password before we run it through our series of tests.
133
+ password.strip!
134
+
135
+ # If no password was specified, raise an exception. This check must
136
+ # occur to avoid a huge security hole if anonymous bind is on - if this
137
+ # check is not performed, someone can authenticate without providing a
138
+ # password when anonymous bind is turned on.
139
+ raise PasswordInvalid unless (!password.nil? and password.length > 0)
140
+
141
+ # Clone our shared connection for isolated use in determining the
142
+ # validity of our user's credentials.
143
+ auth_connection = Base.connection.clone
144
+
145
+ # Unbind the connection if it is already bound.
146
+ auth_connection.unbind if auth_connection.bound?
147
+
148
+ begin
149
+
150
+ # Attempt to bind to the connection as the currently loaded user with
151
+ # the supplied password.
152
+ auth_connection.bind("#{@username}@#{@@server_settings[:domain]}",
153
+ password)
154
+
155
+ return true
156
+
157
+ rescue LDAP::ResultError
158
+ if ($!.to_s == "Invalid credentials")
159
+ raise PasswordInvalid
160
+ else
161
+ raise
162
+ end
163
+ ensure
164
+ auth_connection.unbind
165
+ auth_connection = nil
166
+ end
167
+
168
+ return false
169
+
170
+ end
171
+
172
+ #
173
+ # Conveniently return the name of the User if the object is called
174
+ # directly.
175
+ #
176
+ def to_s #:nodoc:
177
+ @name
178
+ end
179
+
180
+ #
181
+ # Proxy for loading and returning this users' manager.
182
+ #
183
+ def manager #:nodoc:
184
+ @manager ||= User.new(@manager_dn) unless @manager_dn.nil?
185
+ end
186
+
187
+ #
188
+ # Proxy for loading and returning the group membership of this user.
189
+ #
190
+ def groups #:nodoc:
191
+ groups = Array.new
192
+ unless @groups.nil?
193
+ for group_dn in @groups
194
+ groups << Group.new(group_dn)
195
+ end
196
+ end
197
+ groups
198
+ end
199
+
200
+ #
201
+ # Proxy for loading and returning the users who report directly to this
202
+ # user.
203
+ #
204
+ def direct_reports #:nodoc:
205
+ direct_reports = Array.new
206
+ unless @direct_reports.nil?
207
+ for user_dn in @direct_reports
208
+ direct_reports << User.new(user_dn)
209
+ end
210
+ end
211
+ direct_reports
212
+ end
213
+
214
+ #
215
+ # Determines if the user is a member of the given group. Returns true if
216
+ # the user is in the passed group.
217
+ #
218
+ def member_of?(group)
219
+ @groups.include?(group.dn)
220
+ end
221
+
222
+ private
223
+
224
+ #
225
+ # Attempt to load a user by their username (sAMAccountName).
226
+ #
227
+ def load_by_username(username)
228
+
229
+ if username.nil? or username.length == 0
230
+ raise ArgumentError, "No username provided."
231
+ end
232
+
233
+ @username = username
234
+
235
+ # Set our filter to include only information about the requested user of
236
+ # class user.
237
+ filter = "(&(objectClass=user)(sAMAccountName=#{@username}))"
238
+
239
+ entries = Base.search(@@server_settings[:base_dn],
240
+ LDAP::LDAP_SCOPE_SUBTREE, filter, ATTRIBUTES)
241
+
242
+ raise UnknownUserError if (entries.nil? or entries.length != 1)
243
+
244
+ parse_ldap_entry(entries[0])
245
+
246
+ end
247
+
248
+ #
249
+ # Attempt to load a User by its Distinguished Name (DN).
250
+ #
251
+ def load_by_dn(dn)
252
+
253
+ if dn.nil? or dn.length == 0
254
+ raise ArgumentError, "No distinguished name provided."
255
+ end
256
+
257
+ # De-escape the DN
258
+ dn = dn.gsub(/"/, "\\\"")
259
+
260
+ # Set our filter to include only information about the requested user of
261
+ # class user.
262
+ filter = "(&(objectClass=user))"
263
+
264
+ entries = Base.search(dn, LDAP::LDAP_SCOPE_BASE, filter)
265
+
266
+ raise UnknownUserError if (entries.nil? or entries.length != 1)
267
+
268
+ parse_ldap_entry(entries[0])
269
+
270
+ end
271
+
272
+ def parse_ldap_entry(entry)
273
+ @dn = entry['distinguishedName'][0]
274
+ @username = entry['sAMAccountName'][0]
275
+ @name = entry['displayName'][0] unless entry['displayName'].nil?
276
+ @given_name = entry['givenName'][0] unless entry['givenName'].nil?
277
+ @surname = entry['sn'][0] unless entry['sn'].nil?
278
+ @email = entry['mail'][0] unless entry['mail'].nil?
279
+ @manager_dn = entry['manager'][0] unless entry['manager'].nil?
280
+ @company = entry['company'][0] unless entry['company'].nil?
281
+ @department = entry['department'][0] unless entry['department'].nil?
282
+ @title = entry['title'][0] unless entry['title'].nil?
283
+ @main_number = entry['telephoneNumber'][0] unless entry['telephoneNumber'].nil?
284
+ @mobile_number = entry['mobile'][0] unless entry['mobile'].nil?
285
+ @street_address = entry['streetAddress'][0] unless entry['streetAddress'].nil?
286
+ @city = entry['l'][0] unless entry['l'].nil?
287
+ @state = entry['st'][0] unless entry['st'].nil?
288
+ @zip = entry['postalCode'][0] unless entry['postalCode'].nil?
289
+ @country = entry['co'][0] unless entry['co'].nil?
290
+ @direct_reports = entry['directReports']
291
+ @groups = entry['memberOf']
292
+ end
293
+
294
+ end
295
+
296
+ end
@@ -0,0 +1,37 @@
1
+ #--
2
+ # Active Directory Module for Ruby
3
+ #
4
+ # Copyright (c) 2005-2006 Justin Mecham
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to
8
+ # deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ # sell copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ # IN THE SOFTWARE.
23
+ #++
24
+
25
+ module ActiveDirectory
26
+
27
+ module VERSION #:nodoc:
28
+
29
+ MAJOR = 0
30
+ MINOR = 9
31
+ TINY = 3
32
+
33
+ STRING = [MAJOR, MINOR, TINY].join('.')
34
+
35
+ end
36
+
37
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: activedirectory
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.9.3
7
+ date: 2006-05-11 00:00:00 -06:00
8
+ summary: Module for easy interaction with Active Directory servers.
9
+ require_paths:
10
+ - lib
11
+ email: justin@aspect.net
12
+ homepage:
13
+ rubyforge_project:
14
+ description: Makes it trivial to integrate with Active Directory servers for information and authentication concerning users and groups.
15
+ autorequire: active_directory
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Justin Mecham
30
+ files:
31
+ - Rakefile
32
+ - install.rb
33
+ - README
34
+ - LICENSE
35
+ - lib/active_directory
36
+ - lib/active_directory.rb
37
+ - lib/active_directory/base.rb
38
+ - lib/active_directory/ext
39
+ - lib/active_directory/group.rb
40
+ - lib/active_directory/user.rb
41
+ - lib/active_directory/version.rb
42
+ - lib/active_directory/ext/class.rb
43
+ - lib/active_directory/ext/hash.rb
44
+ test_files: []
45
+
46
+ rdoc_options: []
47
+
48
+ extra_rdoc_files: []
49
+
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ requirements:
55
+ - none
56
+ dependencies: []
57
+