admapper 0.0.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,23 @@
1
+ LICENSE
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2010 Brian P. Hogan
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,183 @@
1
+ ==ADMapper
2
+ Library to connect to ActiveDirectory and map your users to ActiveDirectory users.
3
+
4
+ You supply your own classes and mix the User or Group module into your classes.
5
+
6
+ == Why
7
+ Because http://github.com/filefrog/activedirectory didn't work well for me and I wanted
8
+ flexibility to use my own classes, whether those are in Sinatra or
9
+ using full-blown ActiveRecord classes in a Rails app.
10
+
11
+ == Installation
12
+
13
+ It's a gem, so you can
14
+
15
+ gem install admapper
16
+
17
+ Or you can clone the repo and build it yourself with
18
+
19
+ gem build admapper.gemspec
20
+
21
+ == Usage
22
+ Basic usage is simple. Include the gem
23
+
24
+ require 'rubygems'
25
+ require 'admapper'
26
+
27
+ Then add the mixin to your class of choice
28
+
29
+ class User
30
+ attr_accessor: username
31
+ include ADMapper::Resource
32
+ end
33
+
34
+ Finally, configure the connection:
35
+
36
+ ADMapper::Configuration.connection_info = {:username => "homer",
37
+ :password=>"foo",
38
+ :host => "your_ad_host",
39
+ :port=>"636",
40
+ :ssl => true,
41
+ :domain => "example.com"}
42
+
43
+ The connection_info method takes a hash, so you can use a YML file if you'd like:
44
+
45
+ CONFIG_OPTS = YAML::load(File.open(File.expand_path(File.dirname(__FILE__) + "/admapper.yml"))).symbolize_keys
46
+ ADMapper::Configuration.connection_info
47
+
48
+ == Working with Users
49
+
50
+ === Finding someone by username
51
+
52
+ u = User.find_in_ad_by_username("homer")
53
+
54
+ This creates a new instance of your User class. If you're using ActiveRecord, you can save this.
55
+
56
+ === Mapping users
57
+
58
+ We use the "username" column to find stuff in ActiveDirectory by default. But you may not have a username column - you might call it "login". You may also want to easily grab other info from ActiveDirectory and map it to your own objects. In your model, add this method:
59
+
60
+ Simply map your model (keys of the hash) to ActiveDirectory (values of the hash)
61
+
62
+ def ad_map
63
+ {
64
+ :username => :samaccountname,
65
+ :full_name => :displayname
66
+ }
67
+ end
68
+
69
+ When you call the find_in_ad_by_username method, this method gets called.
70
+ You can also access the actual Net::LDAP::Entry object by calling
71
+
72
+ user = User.find_in_ad_by_username("homer")
73
+ user.ad_user
74
+ user.ad_user.samaccountname
75
+ user.ad_user.givenname
76
+
77
+ === Working with an existing user
78
+ So maybe you have a user already.
79
+
80
+ u = User.find(1)
81
+
82
+ And you want his AD information? Fetch it. It'll default to looking up the user in ActiveDirectory by the <tt>username</tt> attribute.
83
+
84
+ u.find_in_ad
85
+
86
+ This will fill in the attributes you specified in your ad_map method.
87
+
88
+ You can use a different field. If you store the username as "login", do this:
89
+
90
+ u.find_in_ad(:key => "login")
91
+
92
+
93
+ === Authenticating a user
94
+
95
+ Don't do this. Taking someone's password and passing it on to Active Directory is just stupid. Use CAS, Shibboleth, or something else that prevents your app from ever seeing a user's password. If you insist on doing this, use SSL, filter the password out of your logs, and pray. This will let you do what you want
96
+
97
+ User.authenticate_with_active_directory("homer", "1234")
98
+
99
+ It'll return true or false. It won't return a user. I assume you'll be wrapping this call in something else that will fetch the user object from your local DB.
100
+
101
+ == Groups
102
+
103
+ Working on group support. Got a few things working:
104
+
105
+ === Getting groups
106
+
107
+ Create your own class
108
+
109
+ class Group
110
+ attr_accessor :name
111
+ include ADMapper::Group
112
+ end
113
+
114
+ Then look for a group
115
+
116
+ g = Group.find_in_ad_by_name("Marketing")
117
+
118
+ You can also look for groups
119
+
120
+ groups = Group.find_all_in_ad_by_name("HR.*")
121
+
122
+ === Users and their groups
123
+
124
+ Need to find all groups for a user?
125
+
126
+ class Group
127
+ attr_accessor :name
128
+ include ADMapper::Group
129
+ end
130
+
131
+ class User
132
+ attr_accessor :name, :username
133
+
134
+ include ADMapper::User
135
+ set_group_class Group
136
+ end
137
+
138
+ Then get a user
139
+
140
+ u = User.find(1)
141
+ u.find_in_ad
142
+ groups = u.groups
143
+
144
+ == Testing
145
+
146
+ I'm not giving you my credentials, so you'll need to supply your own. Create the file
147
+
148
+ test/admapper.yml
149
+
150
+ and put this inside:
151
+
152
+ username: "username"
153
+ password: "password"
154
+ domain: "example.com"
155
+ host: "domain.controller.example.com"
156
+ ssl: true
157
+ port: 636
158
+
159
+ Then open test_helper and change the GROUP constant to a group that your username is a member of.
160
+
161
+ Given good credentials, the tests should pass.
162
+
163
+ == Patches
164
+ * Fork it
165
+ * Write a test
166
+ * Make a fix
167
+ * send a patch or a pull request.
168
+ * Don't touch the changelog.
169
+ * If it doesn't have a test, I'm not looking at it.
170
+
171
+ If this doesn't do what you want it to do, fork this and write your own library. I'm sharing this as a starting point and this is the bare minimum I need to get my AD tasks accomplished.
172
+
173
+ == Changelog
174
+ 0.0.3 (2010-11-08)
175
+ * Added group lookup by name
176
+ * Added ability to get groups for a user
177
+ 0.0.2 (2010-03-31)
178
+ * refactoring to a central configuration option
179
+ * removed ad_connect! class method from resource module. Use a single global connection
180
+ 0.0.1 (2010-03-30)
181
+ * Initial mixin and basic support for finding stuff via AD
182
+ * Mapping objects
183
+ * Tests
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
6
+ require 'ADMapper'
7
+
8
+ desc "Run the tests"
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'test'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ Rake::TestTask.new(:t) do |t|
16
+ t.libs << 'test'
17
+ t.pattern = ENV["TEST"]
18
+ t.verbose = true
19
+ end
20
+
21
+ desc 'Start an IRB session with all necessary files required.'
22
+ task :shell do |t|
23
+ chdir File.dirname(__FILE__)
24
+ exec 'irb -I lib/ -I lib/ADMapper -r rubygems -r net-ldap -r tempfile -r init'
25
+ end
26
+
27
+ desc 'Generate documentation for ADMapper'
28
+ Rake::RDocTask.new(:rdoc) do |rdoc|
29
+ rdoc.rdoc_dir = 'doc'
30
+ rdoc.title = 'ADMapper'
31
+ rdoc.options << '--line-numbers' << '--inline-source'
32
+ rdoc.rdoc_files.include('README*')
33
+ rdoc.rdoc_files.include('lib/**/*.rb')
34
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "admapper")
data/lib/admapper.rb ADDED
@@ -0,0 +1,8 @@
1
+ # ADMapper is a simple wrapper for ActiveRecord user lookup and authentication.
2
+ require 'yaml'
3
+ require 'net/ldap'
4
+ require 'admapper/extensions'
5
+ require 'admapper/configuration'
6
+ require 'admapper/connection'
7
+ require 'admapper/user'
8
+ require 'admapper/group'
@@ -0,0 +1,13 @@
1
+ module ADMapper
2
+ # Holds configuration options.
3
+ class Configuration
4
+
5
+ @connection_info = nil
6
+ class << self; attr_accessor :connection_info; end
7
+
8
+ end
9
+
10
+ # Configuration exception
11
+ class ConfigurationMissingError < StandardError
12
+ end
13
+ end
@@ -0,0 +1,69 @@
1
+ module ADMapper
2
+ class Connection
3
+ @@ad_connection = nil
4
+ @@ad_treebase = nil
5
+ @@ad_user_filter = nil
6
+
7
+ def self.treebase
8
+ @@ad_treebase
9
+ end
10
+
11
+ # class method to return the current connection
12
+ def self.current_connection
13
+ self.ad_connect_if_not_connected
14
+ @@ad_connection
15
+ end
16
+
17
+ def self.current_connection=(conn)
18
+ @@ad_connection = conn
19
+ end
20
+
21
+ # config_options = {:username => "homer",
22
+ # :password => "1234",
23
+ # :domain => "springfieldnuclear.com",
24
+ # :host => "ad.springfieldnuclear.com",
25
+ # :ssl => true,
26
+ # :port => 636}
27
+ #
28
+ # if ad_connect!(config_options)
29
+ # ....
30
+ # else
31
+ # ...
32
+ # end
33
+ def self.ad_connect!(config = nil)
34
+ config ||= ADMapper::Configuration.connection_info
35
+
36
+ raise ADMapper::ConfigurationMissingError if config.nil?
37
+ username = config[:username]
38
+ password = config[:password]
39
+ domain = config[:domain]
40
+ host = config[:host]
41
+ port = config[:port]
42
+ dc1 = domain.split(".").first
43
+ dc2 = domain.split(".").last
44
+ ssl = config[:ssl] ? true : false
45
+ self.current_connection = initialize_ldap_con(username + "@#{domain}", password, host, port, ssl)
46
+ @@ad_treebase = "DC=#{dc1},DC=#{dc2}"
47
+ @@ad_user_filter = Net::LDAP::Filter.eq( "sAMAccountName", username )
48
+ self.current_connection.bind
49
+ end
50
+
51
+ # initializes the connection to the ldap server. Does not bind.
52
+ def self.initialize_ldap_con(user_name, password, host, port, ssl)
53
+ ldap = Net::LDAP.new
54
+ ldap.host = host
55
+ ldap.port = port #required for SSL connections, 389 is the default plain text port
56
+ if ssl
57
+ ldap.encryption :simple_tls #also required to tell Net:LDAP that we want SSL
58
+ end
59
+ ldap.auth "#{user_name}","#{password}"
60
+ ldap #explicitly return the ldap connection object
61
+ end
62
+
63
+ # connects if not connected already
64
+ def self.ad_connect_if_not_connected
65
+ self.ad_connect! if @@ad_connection.nil?
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def symbolize_keys
3
+ inject({}) do |options, (key, value)|
4
+ options[(key.to_sym rescue key) || key] = value
5
+ options
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,90 @@
1
+ module ADMapper
2
+
3
+ module Group
4
+
5
+ def self.included(model)
6
+ model.class_eval do
7
+ extend ADMapper::Group::ClassMethods
8
+ include ADMapper::Group::InstanceMethods
9
+ attr_accessor :ad_group
10
+ end
11
+
12
+ end
13
+
14
+ module InstanceMethods
15
+
16
+ # maps the ad_user's attributes to your class' attributes
17
+ #. Implement the ad_map method in your own class to control how fields map.
18
+ def map_group_from_ad(ad_group)
19
+ self.ad_map.each do |group_object_field, ad_object_field|
20
+ self.send("#{group_object_field}=", ad_group.send(ad_object_field).to_s)
21
+ end
22
+ self.ad_group = ad_group
23
+ end
24
+
25
+ # Default mapping of user object to active directory.
26
+ # You will most likely want to implement this in your own class
27
+ # instead of using this very basic default.
28
+ # Simply map your model (keys of the hash) to ActiveDirectory (values of the hash)
29
+ #
30
+ # def ad_map
31
+ # {
32
+ # :username => :samaccountname,
33
+ # :full_name => :displayname
34
+ # }
35
+ # end
36
+ def ad_map
37
+ {:name => :cn}
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+
43
+ def find_in_ad_by_name(groupname)
44
+ attrs = [ "cn", "ou" , "objectClass"]
45
+ filter = Net::LDAP::Filter.eq( "objectClass", "group" )
46
+ name_filter = Net::LDAP::Filter.eq( "cn", groupname )
47
+ ad_connection = ADMapper::Connection.current_connection
48
+ ad_group = nil
49
+ ad_connection.search( :base => ADMapper::Connection.treebase,
50
+ :attributes => attrs,
51
+ :filter => filter & name_filter ) do |entry|
52
+ if entry
53
+ ad_group = self.new
54
+ ad_group.map_group_from_ad(entry)
55
+ break
56
+ end
57
+ end
58
+ ad_group
59
+
60
+ end
61
+ def find_all_in_ad_by_name(groupname)
62
+
63
+ attrs = [ "cn", "ou" , "objectClass"]
64
+ filter = Net::LDAP::Filter.eq( "objectClass", "group" )
65
+ name_filter = Net::LDAP::Filter.eq( "cn", groupname )
66
+ ad_connection = ADMapper::Connection.current_connection
67
+ ad_groups = []
68
+
69
+ ad_connection.search( :base => ADMapper::Connection.treebase,
70
+ :attributes => attrs,
71
+ :filter => filter & name_filter ) do |entry|
72
+ ad_group = entry
73
+ if ad_group
74
+ group = self.new
75
+ group.map_group_from_ad(ad_group)
76
+ ad_groups << group
77
+ end
78
+
79
+
80
+ end
81
+
82
+ ad_groups
83
+
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,165 @@
1
+ module ADMapper
2
+
3
+ module User
4
+
5
+ def self.included(model)
6
+ model.class_eval do
7
+ extend ADMapper::User::ClassMethods
8
+ include ADMapper::User::InstanceMethods
9
+ attr_accessor :ad_user
10
+ end
11
+
12
+ end
13
+
14
+ module InstanceMethods
15
+
16
+ # Returns the groups for a user.
17
+ def groups
18
+ groups = []
19
+
20
+ search_filter = Net::LDAP::Filter.eq("sAMAccountName", self.ad_user.samaccountname.first)
21
+ ad_connection = ADMapper::Connection.current_connection
22
+ ad_connection.search(:base => ADMapper::Connection.treebase,
23
+ :filter => search_filter) do |entry|
24
+
25
+ entry.each do |attribute, values|
26
+ if attribute.to_s.match(/memberof/)
27
+ values.each do |value|
28
+ a = value.split(',')
29
+ md = a[0].match(/CN=(.+)/)
30
+
31
+ groups << md[1]
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ groups.collect do |g|
38
+ self.class.group_class.find_in_ad_by_name(g)
39
+ end.compact
40
+
41
+
42
+ end
43
+
44
+
45
+
46
+ def member_of?(group)
47
+ group_member = false
48
+
49
+ search_filter = Net::LDAP::Filter.eq("sAMAccountName", self.ad_user.samaccountname.first)
50
+ ad_connection = ADMapper::Connection.current_connection
51
+ ad_connection.search(:base => ADMapper::Connection.treebase,
52
+ :filter => search_filter) do |entry|
53
+
54
+ entry.each do |attribute, values|
55
+ if attribute.to_s.match(/memberof/)
56
+ values.each do |value|
57
+ a = value.split(',')
58
+ md = a[0].match(/CN=(.+)/)
59
+
60
+ # user is a member of the right group
61
+ if md[1] == group
62
+ group_member = true
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ group_member
69
+
70
+ end
71
+
72
+
73
+ # finds a user in active directory using the internal key.
74
+ # Defaults to username
75
+ def find_in_ad(options = {:key => :username})
76
+ username = send(options[:key])
77
+ ad_user = self.class.ad_query_by_username(username)
78
+ return nil if ad_user.nil?
79
+ self.map_user_from_ad(ad_user)
80
+ true
81
+ end
82
+
83
+ # maps the ad_user's attributes to your class' attributes
84
+ #. Implement the ad_map method in your own class to control how fields map.
85
+ def map_user_from_ad(ad_user)
86
+ self.ad_map.each do |user_object_field, ad_object_field|
87
+ self.send("#{user_object_field}=", ad_user.send(ad_object_field).to_s)
88
+ end
89
+ self.ad_user = ad_user
90
+ end
91
+
92
+ # Default mapping of user object to active directory.
93
+ # You will most likely want to implement this in your own class
94
+ # instead of using this very basic default.
95
+ # Simply map your model (keys of the hash) to ActiveDirectory (values of the hash)
96
+ #
97
+ # def ad_map
98
+ # {
99
+ # :username => :samaccountname,
100
+ # :full_name => :displayname
101
+ # }
102
+ # end
103
+ def ad_map
104
+ {:username => :samaccountname}
105
+ end
106
+
107
+ end
108
+
109
+ module ClassMethods
110
+ attr_accessor :group_class
111
+
112
+ def set_group_class(group_class)
113
+ self.group_class = group_class
114
+ end
115
+
116
+ # Authenticating users:
117
+ # Don't do this. Taking someone's password and passing it
118
+ # on to Active Directory is just stupid. Use CAS, Shibboleth, or
119
+ # something else that prevents your app from ever seeing a user's password.
120
+ # If you insist on doing this, use SSL, filter the password out of your logs,
121
+ # and pray. This will let you do what you want
122
+ #
123
+ # User.authenticate_with_active_directory("homer", "1234")
124
+ #
125
+ # It'll return true or false. It won't return a user. I assume you'll be wrapping this call in something else that will fetch the user object from your local DB.
126
+ def authenticate_with_active_directory(username, password)
127
+ auth_ldap = ADMapper::Connection.current_connection.dup.bind_as(
128
+ :filter => Net::LDAP::Filter.eq( "sAMAccountName", username ),
129
+ :base => ADMapper::Connection.treebase,
130
+ :password => password
131
+ )
132
+
133
+ end
134
+
135
+ # Find a user in AD by the given username
136
+ # Calls #map_user_from_ad on the returned results
137
+ # so you can manage it yourself.
138
+ def find_in_ad_by_username(username)
139
+ ad_user = ad_query_by_username(username)
140
+ return nil if ad_user.nil?
141
+
142
+ user = self.new
143
+ user.map_user_from_ad(ad_user)
144
+ user
145
+ end
146
+
147
+ # find a user in AD by the given userame.
148
+ # Connects if not connected
149
+ # Returns an AD object
150
+ def ad_query_by_username(username)
151
+
152
+ user = nil
153
+ search_filter = Net::LDAP::Filter.eq( "sAMAccountName", username )
154
+ ad_connection = ADMapper::Connection.current_connection
155
+ ad_connection.search(:base => ADMapper::Connection.treebase,
156
+ :filter => search_filter,
157
+ :attributes => ['dn','sAMAccountName','displayname','SN','givenName']) do |ad_user|
158
+ user = ad_user
159
+ end
160
+ user
161
+ end
162
+
163
+ end
164
+ end
165
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: admapper
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 3
10
+ version: 0.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Brian Hogan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-08 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: net-ldap
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 0
32
+ - 1
33
+ - 1
34
+ version: 0.1.1
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: mocha
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - "="
44
+ - !ruby/object:Gem::Version
45
+ hash: 43
46
+ segments:
47
+ - 0
48
+ - 9
49
+ - 8
50
+ version: 0.9.8
51
+ type: :development
52
+ version_requirements: *id002
53
+ description:
54
+ email: brianhogan@napcs.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files:
60
+ - README.rdoc
61
+ files:
62
+ - README.rdoc
63
+ - LICENSE
64
+ - Rakefile
65
+ - init.rb
66
+ - lib/admapper.rb
67
+ - lib/admapper/extensions.rb
68
+ - lib/admapper/connection.rb
69
+ - lib/admapper/group.rb
70
+ - lib/admapper/configuration.rb
71
+ - lib/admapper/user.rb
72
+ has_rdoc: true
73
+ homepage: http://www.napcs.com/projects/
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options:
78
+ - --line-numbers
79
+ - --inline-source
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ requirements:
101
+ - net-ldap
102
+ rubyforge_project: admapper
103
+ rubygems_version: 1.3.7
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Friendly mixin for working with Microsoft's ActiveDirectory
107
+ test_files: []
108
+