Skip to content

Commit bc98eb1

Browse files
committed
Initial Commit
0 parents  commit bc98eb1

11 files changed

+305
-0
lines changed

MIT-LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2009 Koziarski Software Ltd
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.

README

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
ActiveDirectoryAuth
2+
===================
3+
4+
Lets you authenticate users against an ActiveDirectory systems, also provides a test implementation so you don't need to have an LDAP server when developing. The goal here is to allow you to store your user information in your local database, but use the LDAP information for authentication and retrieving authorization information.
5+
6+
This will add an 'authenticate' method to your model which works as follows:
7+
8+
@user = User.authenticate("koz", "mypassword") # returns the user
9+
@user = User.authenticate("who knows", "not a password") # nil
10+
11+
In order to map from the ldap result to your database, you need to implement a find_from_ldap method on the user class. It will be passed an ldap user, which responds to a few key methods. +username+ returns the account name that successfully authenticated. +roles+ returns the list of configured group which the user is a member of. It also adds some simple predicate methods for each of the available roles. For example:
12+
13+
def self.find_from_ldap(ldap_user)
14+
returning find_or_create_by_login(ldap_user.username) do |user|
15+
user.permissions.clear
16+
if ldap_user.admin?
17+
user.admin = true
18+
end
19+
if ldap_user.printer_operator?
20+
user.permissions.create! :code=>"printer"
21+
end
22+
user.save!
23+
end
24+
end
25+
26+
There's obviously a risk that users who have been disabled in ldap could still have an active session in the rails application.
27+
28+
Example
29+
=======
30+
31+
in production.rb you can write:
32+
33+
User.authenticates_with_active_directory do |config|
34+
config.host = "ldap.example.com"
35+
config.base_dn = "DC=example,DC=com"
36+
# Only needed if your ldap server doesn't support anonymous binding
37+
config.administrator_dn = "CN=administrator,OU=Administrators Group,OU=Systems"
38+
config.administrator_password = "admin"
39+
40+
config.roles :printer_operator => "CN=Printer Operators,CN=Groups",
41+
:backup_operator => "CN=Backup Operators,CN=Groups",
42+
:admin => "CN=Administrators,CN=Groups"
43+
end
44+
45+
then in development.rb
46+
47+
User.stub_active_directory_authentication do |config|
48+
config.user "koz", "a password", :printer_operator, :backup_operator
49+
config.user "dhh", "anotherpassword", :admin
50+
end
51+
52+
53+
Copyright (c) 2009 Koziarski Software Ltd, released under the MIT license

Rakefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
require 'rake'
2+
require 'rake/testtask'
3+
require 'rake/rdoctask'
4+
5+
desc 'Default: run unit tests.'
6+
task :default => :test
7+
8+
desc 'Test the active_directory_auth plugin.'
9+
Rake::TestTask.new(:test) do |t|
10+
t.libs << 'lib'
11+
t.libs << 'test'
12+
t.pattern = 'test/**/*_test.rb'
13+
t.verbose = true
14+
end
15+
16+
desc 'Generate documentation for the active_directory_auth plugin.'
17+
Rake::RDocTask.new(:rdoc) do |rdoc|
18+
rdoc.rdoc_dir = 'rdoc'
19+
rdoc.title = 'ActiveDirectoryAuth'
20+
rdoc.options << '--line-numbers' << '--inline-source'
21+
rdoc.rdoc_files.include('README')
22+
rdoc.rdoc_files.include('lib/**/*.rb')
23+
end

init.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Include hook code hereacti
2+
ActiveRecord::Base.extend(ActiveDirectoryAuth::BaseMethods)

install.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Install hook code here

lib/active_directory_auth.rb

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# ActiveDirectoryAuth
2+
3+
require 'net/ldap'
4+
5+
module ActiveDirectoryAuth
6+
class LdapConnection
7+
attr_accessor :host, :base_dn, :administrator_dn, :administrator_password,
8+
:fetched_attributes
9+
10+
11+
def initialize(clazz)
12+
@clazz = clazz
13+
@host = "localhost"
14+
@fetched_attributes = ['dn','sAMAccountName','displayname','SN','givenName', 'memberOf']
15+
end
16+
17+
def roles(mapping)
18+
@roles_mapping = mapping
19+
end
20+
21+
def authenticate(username, password)
22+
ldap = new_ldap_connection
23+
ldap.host = @host
24+
25+
if @administrator_dn
26+
Rails.logger.info([@administrator_dn, @base_dn].join(','))
27+
ldap.auth [@administrator_dn, @base_dn].join(','), @administrator_password
28+
end
29+
30+
if results = ldap.bind_as(:base => @base_dn,
31+
:filter => new_ldap_criteria(username),
32+
:attributes => @fetched_attributes,
33+
:password => password)
34+
ad_user = results.first
35+
Rails.logger.info "Successfully bound as #{username.inspect}"
36+
Rails.logger.info "Found #{ad_user.inspect}"
37+
@clazz.find_from_ldap(LdapUser.new(ad_user.samaccountname, map_roles(ad_user.memberOf)))
38+
else
39+
Rails.logger.info "Failed to bind as #{username.inspect} Error: #{ldap.get_operation_result.inspect}"
40+
nil
41+
end
42+
end
43+
44+
def new_ldap_connection
45+
Net::LDAP.new
46+
end
47+
48+
def new_ldap_criteria(username)
49+
Net::LDAP::Filter.eq( "sAMAccountName", username )
50+
end
51+
52+
def map_roles(roles)
53+
@roles_mapping.each_with_object(Set.new) do |pair, memo|
54+
pretty_name, ldap_name = pair
55+
if roles.include?(ldap_name)|| roles.include?([ldap_name, @base_dn].join(','))
56+
memo << pretty_name
57+
end
58+
end
59+
end
60+
end
61+
62+
class StubConnection
63+
def initialize(clazz)
64+
@clazz = clazz
65+
@credentials = {}
66+
end
67+
68+
def user(username, password, *roles)
69+
@credentials[username] = StubUser.new(username, password, roles)
70+
end
71+
72+
def authenticate(username, password)
73+
if (user = @credentials[username]) && user.password == password
74+
return @clazz.find_from_ldap(ad_user)
75+
else
76+
return nil
77+
end
78+
end
79+
end
80+
81+
class LdapUser
82+
attr_reader :username, :roles
83+
84+
def initialize(username, roles)
85+
@username, @roles = username, roles
86+
end
87+
88+
def method_missing(name, *args)
89+
name_s = name.to_s
90+
if name_s.ends_with?("?")
91+
@roles.include? name_s[0..-2].to_sym
92+
else
93+
super
94+
end
95+
end
96+
end
97+
98+
99+
class StubUser < LdapUser
100+
attr_reader :password
101+
102+
def initialize(username, password, roles)
103+
super(username, roles)
104+
@password = password
105+
end
106+
end
107+
108+
module BaseMethods
109+
def authenticates_with_active_directory
110+
yield @ad_config = ActiveDirectoryAuth::LdapConnection.new(self)
111+
extend Authenticate
112+
end
113+
114+
def stub_active_directory_authentication
115+
yield @ad_config = ActiveDirectoryAuth::StubConnection.new(self)
116+
extend Authenticate
117+
end
118+
119+
end
120+
121+
module Authenticate
122+
def authenticate(user, password)
123+
@ad_config.authenticate(user, password)
124+
end
125+
126+
def find_from_ldap(ldap_user)
127+
find_by_login(ldap_user.username)
128+
end
129+
end
130+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# desc "Explaining what the task does"
2+
# task :active_directory_auth do
3+
# # Task goes here
4+
# end

test/active_directory_auth_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require 'test_helper'
2+
3+
User.authenticates_with_active_directory do |config|
4+
config.host = "ldap.example.com"
5+
config.base_dn = "DC=example,DC=com"
6+
config.administrator_dn = "CN=administrator,OU=Administrators Group,OU=Systems"
7+
config.administrator_password = "admin"
8+
9+
config.roles :printer_operator => "CN=Printer Operators,CN=Groups",
10+
:backup_operator => "CN=Backup Operators,CN=Groups",
11+
:admin => "CN=Administrators,CN=Groups"
12+
end
13+
14+
def User.find_from_ldap(ldap_user)
15+
ldap_user
16+
end
17+
18+
class ActiveDirectoryAuthTest < ActiveSupport::TestCase
19+
def setup
20+
@ldap_connection = mock()
21+
ActiveDirectoryAuth::LdapConnection.any_instance.
22+
expects(:new_ldap_connection).
23+
returns(@ldap_connection)
24+
#
25+
@criteria = mock()
26+
ActiveDirectoryAuth::LdapConnection.any_instance.
27+
expects(:new_ldap_criteria).
28+
returns(@criteria).with("koz")
29+
30+
@ldap_connection.expects(:host=).with("ldap.example.com")
31+
# @ldap_connection.expects(:host=).with("ldap.example.com")
32+
@ldap_connection.expects(:auth).with('CN=administrator,OU=Administrators Group,OU=Systems,DC=example,DC=com', 'admin')
33+
@ldap_connection.expects(:bind_as).
34+
with(:password => 'password',
35+
:base => 'DC=example,DC=com',
36+
:filter => @criteria,
37+
:attributes => ['dn', 'sAMAccountName', 'displayname', 'SN', 'givenName', 'memberOf']).
38+
returns(stub(:samaccountname=>"koz", :memberOf=>["CN=Printer Operators,CN=Groups", "CN=Backup Operators,CN=Groups,DC=example,DC=com"]))
39+
40+
end
41+
42+
test "authenticate does what it should" do
43+
ldap_user = User.authenticate("koz", "password")
44+
assert ldap_user.printer_operator?
45+
assert ldap_user.backup_operator?
46+
assert !ldap_user.admin?
47+
end
48+
end

test/test_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require 'rubygems'
2+
require 'active_support'
3+
require 'active_support/test_case'

test/user_models_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
require 'test_helper'
2+
3+
class UserModelsTest < ActiveSupport::TestCase
4+
def setup
5+
end
6+
include ActiveDirectoryAuth
7+
8+
test "Stub user roles work right" do
9+
@stub_user = StubUser.new("koz", "password", [:admin, :librarian])
10+
assert @stub_user.admin?
11+
assert @stub_user.librarian?
12+
assert !@stub_user.hax?
13+
assert_equal [:admin, :librarian].to_set,
14+
@stub_user.roles.to_set
15+
end
16+
17+
# test ""
18+
19+
20+
end

0 commit comments

Comments
 (0)