devise_ldap_multiple 0.9.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 76c06c80d33a10a9a5d8b456c11922d628ca15bd
4
+ data.tar.gz: 1064d2b788a02aa146001923e9ca9476db08c818
5
+ SHA512:
6
+ metadata.gz: f5041d2f9b051e019d5e4377578f79490bb4bf47211278572038b1730d8a4d18373177712d34f0095dec52b488e1c97e19e1e31d4a64d99fcc30d6fda0b0931a
7
+ data.tar.gz: a66484274528ae0a72dac12292dc8e87aaa3fa00864ed3b9e6159902c5351c3fae53be7d1a755c13e949bcd7cea389575668b63d7807cdd791f10e8e8643b87e
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ log
4
+ *.sqlite3
5
+ test/ldap/openldap-data/*
6
+ !test/ldap/openldap-data/run
7
+ test/ldap/openldap-data/run/slapd.*
8
+ test/rails_app/tmp
9
+ pkg/*
10
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'debugger', platform: :ruby_19
7
+ gem 'byebug', platform: :ruby_20
8
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Scott Willett
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.md ADDED
@@ -0,0 +1,60 @@
1
+ Devise LDAP Multiple
2
+ ====================
3
+
4
+ This project is a fork of the brilliant project devise_ldap_authenticatable here: (https://github.com/cschiewek/devise_ldap_authenticatable).
5
+
6
+ The difference is that this project allows you to use multiple LDAP databases in a single app, and the code was refactored significantly while adding this feature.
7
+
8
+ Devise LDAP Multiple is a LDAP based authentication strategy for the [Devise](http://github.com/plataformatec/devise) authentication framework.
9
+
10
+ If you are building applications for use within your organization which require authentication and you want to use LDAP, this plugin is for you.
11
+
12
+ Devise LDAP Multiple works in replacement of Database Authenticatable. This devise plugin has not been tested with DatabaseAuthenticatable enabled at the same time. This is meant as a drop in replacement for DatabaseAuthenticatable allowing for a semi single sign on approach.
13
+
14
+ Prerequisites
15
+ -------------
16
+
17
+ * devise ~> 3.0.0 (which requires rails ~> 4.0)
18
+ * net-ldap ~> 0.6.0
19
+
20
+ If you are transitioning from having Devise manage your users' passwords in the database to using LDAP auth, you may have to update your `users` table to make `encrypted_password` nullable, or else the LDAP user insert will fail.
21
+
22
+ Usage
23
+ -----
24
+ In the Gemfile for your application:
25
+
26
+ gem "devise_ldap_multiple"
27
+
28
+ To get the latest version, pull directly from github instead of the gem:
29
+
30
+ gem "devise_ldap_multiple", :git => "https://github.com/xarael/devise_ldap_multiple"
31
+
32
+ Setup
33
+ -----
34
+
35
+ Run the rails generators for devise (please check the [devise](http://github.com/plataformatec/devise) documents for further instructions)
36
+
37
+ rails generate devise:install
38
+ rails generate devise MODEL_NAME
39
+
40
+ Run the rails generator for `devise_ldap_multiple`
41
+
42
+ rails generate devise_ldap_multiple:install
43
+ rails generate devise_ldap_multiple [MODEL_NAME]
44
+
45
+ MODEL_NAME defaults to: user
46
+
47
+ This will install [MODEL_NAME].yml file to the config/ldap/ directory, update the devise.rb initializer with a default scope (default), and update your model.
48
+
49
+ All configuration is set within these .yml files for each model. There's comments in the files to describe the various settings.
50
+
51
+ References
52
+ ----------
53
+ * [Devise LDAP Authenticatable](https://github.com/cschiewek/devise_ldap_authenticatable)
54
+ * [OpenLDAP](http://www.openldap.org/)
55
+ * [Devise](http://github.com/plataformatec/devise)
56
+ * [Warden](http://github.com/hassox/warden)
57
+
58
+ Released under the MIT license
59
+
60
+ Copyright (c) 2016 [Scott Willett](https://github.com/xarael)
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require File.expand_path('spec/rails_app/config/environment', File.dirname(__FILE__))
2
+ require 'rdoc/task'
3
+
4
+ desc 'Default: run test suite.'
5
+ task :default => :spec
6
+
7
+ desc 'Generate documentation for the devise_ldap_multiple plugin.'
8
+ Rake::RDocTask.new(:rdoc) do |rdoc|
9
+ rdoc.rdoc_dir = 'rdoc'
10
+ rdoc.title = 'DeviseLDAPMultiple'
11
+ rdoc.options << '--line-numbers' << '--inline-source'
12
+ rdoc.rdoc_files.include('README.md')
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ end
15
+
16
+ RailsApp::Application.load_tasks
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "devise_ldap_multiple/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'devise_ldap_multiple'
7
+ s.version = DeviseLdapMultiple::VERSION.dup
8
+ s.platform = Gem::Platform::RUBY
9
+ s.summary = 'Devise extension to allow authentication to multiple LDAPs. Fork of the devise_ldap_authenticatable project.'
10
+ s.email = 'swillett@outlook.com'
11
+ s.homepage = 'https://github.com/xarael/devise_ldap_multiple'
12
+ s.description = s.summary
13
+ s.authors = ['Curtis Schiewek', 'Daniel McNevin', 'Steven Xu', 'Scott Willett']
14
+ s.license = 'MIT'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ # s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'devise', '>= 3.4.1'
22
+ s.add_dependency 'net-ldap', '>= 0.6', '!= 0.12'
23
+
24
+ s.add_development_dependency 'rake', '>= 0.9'
25
+ s.add_development_dependency 'rdoc', '>= 3'
26
+ s.add_development_dependency 'rails', '>= 4.0'
27
+ s.add_development_dependency 'sqlite3'
28
+ s.add_development_dependency 'factory_girl_rails', '~> 1.0'
29
+ s.add_development_dependency 'factory_girl', '~> 2.0'
30
+ s.add_development_dependency 'rspec-rails'
31
+
32
+ %w{database_cleaner capybara launchy}.each do |dep|
33
+ s.add_development_dependency(dep)
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require 'devise'
3
+ require 'net/ldap'
4
+
5
+ require 'devise_ldap_multiple/exception'
6
+ require 'devise_ldap_multiple/logger'
7
+ require 'devise_ldap_multiple/ldap/adapter'
8
+ require 'devise_ldap_multiple/ldap/connection'
9
+
10
+ # Nearly all configuration is in each scope.yml file under config/ldap/ for each scope.
11
+ module Devise
12
+
13
+ # The default scope to use if devise doesn't map to a scope and a scope isn't manually specified
14
+ # Can be overwritten by setting in file config/initializers/devise.rb
15
+ mattr_accessor :ldap_default_scope
16
+ @@ldap_default_scope = 'default'
17
+
18
+ end
19
+
20
+ # Add ldap_authenticatable strategy to defaults.
21
+ Devise.add_module(:ldap_authenticatable,
22
+ :route => :session, ## This will add the routes, rather than in the routes.rb
23
+ :strategy => true,
24
+ :controller => :sessions,
25
+ :model => 'devise_ldap_multiple/model')
@@ -0,0 +1,6 @@
1
+ module DeviseLdapMultiple
2
+
3
+ class LdapException < Exception
4
+ end
5
+
6
+ end
@@ -0,0 +1,111 @@
1
+ require "net/ldap"
2
+
3
+ module Devise
4
+
5
+ module LDAP
6
+
7
+ DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY = 'uniqueMember'
8
+
9
+ # Establishes connections and interacts with LDAP. Uses Connection objects to do this.
10
+ # Can interact with these methods in rails
11
+ module Adapter
12
+
13
+ # Returns all attributes for an account from LDAP
14
+ def self.get_ldap_entry(login, scope = default_scope)
15
+ self.ldap_connect(login, scope).search_for_login
16
+ end
17
+
18
+ # Get the value of an attribute for an account from LDAP
19
+ def self.get_ldap_param(login, param, scope = default_scope)
20
+ resource = self.ldap_connect(login, scope)
21
+ resource.ldap_param_value(param)
22
+ end
23
+
24
+ # Returns the DistinguishedName of an account (regardless of if it exists or not)
25
+ def self.get_dn(login, scope = default_scope)
26
+ self.ldap_connect(login, scope).dn
27
+ end
28
+
29
+ def self.password_updatable? (login, scope = default_scope)
30
+ options = { login: login, scope: scope }
31
+ resource = Devise::LDAP::Connection.new(options)
32
+ resource.password_updatable?
33
+ end
34
+
35
+ def self.user_creatable? (login, scope = default_scope)
36
+ options = { login: login, scope: scope }
37
+ resource = Devise::LDAP::Connection.new(options)
38
+ resource.user_creatable?
39
+ end
40
+
41
+ # Boolean returned for if an account in the LDAP exists (doesn't check authentication / authorization): false if a valid match can't be obtained from ldap.
42
+ def self.valid_login?(login, scope = default_scope)
43
+ self.ldap_connect(login, scope).valid_login?
44
+ end
45
+
46
+ # Tries to authenticate credentails to LDAP. Returns true or false appropriately.
47
+ def self.valid_credentials?(login, password_plaintext, scope = default_scope)
48
+ options = { login: login, password: password_plaintext, scope: scope }
49
+ resource = Devise::LDAP::Connection.new(options)
50
+ resource.authorized?
51
+ end
52
+
53
+ # Returns true or false depending on if the users credentials have expired or not
54
+ def self.expired_valid_credentials?(login, password_plaintext, scope = default_scope)
55
+ options = { login: login, password: password_plaintext, scope: scope }
56
+ resource = Devise::LDAP::Connection.new(options)
57
+ resource.expired_valid_credentials?
58
+ end
59
+
60
+ # Updates a users password in LDAP
61
+ def self.update_password(login, new_password, scope = default_scope)
62
+ options = { login: login, new_password: new_password, scope: scope }
63
+ resource = Devise::LDAP::Connection.new(options)
64
+ resource.change_password! if new_password.present?
65
+ end
66
+
67
+ # Also updates the password. Unsure what differentiates this from update_password currently.
68
+ def self.update_own_password(login, new_password, current_password)
69
+ set_ldap_param(login, :userPassword, new_password, current_password, true)
70
+ end
71
+
72
+ # Returns a list of group memberships for a user
73
+ def self.get_groups(login, scope = default_scope)
74
+ self.ldap_connect(login, scope).user_groups
75
+ end
76
+
77
+ # Checks if a user is a member of a specific group
78
+ def self.in_ldap_group?(login, group_name, group_attribute = nil, scope = default_scope)
79
+ self.ldap_connect(login, scope).in_group?(group_name, group_attribute)
80
+ end
81
+
82
+ # Sets an LDAP attribute for an account to a new value
83
+ def self.set_ldap_param(login, param, new_value, password = nil, scope = default_scope)
84
+ options = { login: login, password: password, scope: scope }
85
+ resource = Devise::LDAP::Connection.new(options)
86
+ resource.set_param(param, new_value)
87
+ end
88
+
89
+ # Deletes the LDAP attribute for an account
90
+ def self.delete_ldap_param(login, param, password = nil, scope = default_scope)
91
+ options = { login: login, password: password, scope: scope }
92
+ resource = Devise::LDAP::Connection.new(options)
93
+ resource.delete_param(param)
94
+ end
95
+
96
+ # Creates a new connection to an LDAP database and returns the connection object to run methods against
97
+ def self.ldap_connect(login, scope = default_scope)
98
+ options = { login: login, scope: scope }
99
+ Devise::LDAP::Connection.new(options)
100
+ end
101
+
102
+ # The default scope to use if none is specified
103
+ def self.default_scope
104
+ ::Devise.ldap_default_scope
105
+ end
106
+
107
+ end # module Adapter
108
+
109
+ end # module LDAP
110
+
111
+ end # module Devise
@@ -0,0 +1,326 @@
1
+ module Devise
2
+
3
+ module LDAP
4
+
5
+ class Connection
6
+
7
+ attr_reader :ldap, :login
8
+
9
+ def initialize(params = {})
10
+
11
+ # Option scope determines the mapping_file to use
12
+ @scope = params[:scope]
13
+ config_file_path = "#{Rails.root}/config/ldap/#{@scope}.yml"
14
+
15
+ # Read the config file depending on the scope
16
+ ldap_config = YAML.load(ERB.new(File.read(config_file_path)).result)
17
+
18
+ # Alter the ssl option in the configuration
19
+ ldap_config[Rails.env]["ssl"] = :simple_tls if ldap_config[Rails.env]["ssl"] === true
20
+
21
+ # Determines if logging happens
22
+ @logging_enabled = ldap_config['logging_enabled']
23
+
24
+ # Evaluated into Proc objects
25
+ @auth_username_builder = eval ldap_config['auth_username_builder']
26
+ @auth_password_builder = eval ldap_config['auth_password_builder']
27
+
28
+ # Attributes and groups required for authorisation of an account
29
+ @group_base = ldap_config["group_base"]
30
+ @required_groups = ldap_config["required_groups"]
31
+ @group_membership_attribute = ldap_config.has_key?("group_membership_attribute") ? ldap_config["group_membership_attribute"] : "uniqueMember"
32
+ @required_attributes = ldap_config["require_attribute"]
33
+ @required_attributes_presence = ldap_config["require_attribute_presence"]
34
+
35
+ # Various Flags
36
+ @allow_unauthenticated_bind = ["allow_unauthenticated_bind"]
37
+ @check_attributes = ldap_config['check_attributes']
38
+ @check_attributes_presence = ldap_config['check_attributes_presence']
39
+ @use_admin_to_bind = ldap_config['use_admin_to_bind']
40
+ @check_group_membership = ldap_config["check_group_membership"]
41
+ @check_group_membership_without_admin = ldap_config["check_group_membership_without_admin"]
42
+ @update_passwords = ldap_config['update_passwords']
43
+ @create_user = ldap_config['create_user']
44
+
45
+ # Other params referencing credentails not part of the config file
46
+ @login = params[:login]
47
+ @password = params[:password]
48
+ @new_password = params[:new_password]
49
+
50
+ # Build up the options used to establish an LDAP connection
51
+ ldap_options = {}
52
+ ldap_options[:login] = params[:login] if params[:login]
53
+ ldap_options[:password] = params[:password] if params[:password]
54
+ ldap_options[:new_password] = params[:new_password] if params[:new_password]
55
+ ldap_options[:ldap_auth_username_builder] = @auth_username_builder
56
+ ldap_options[:admin] = @use_admin_to_bind
57
+ ldap_options[:encryption] = ldap_config[Rails.env]["ssl"].to_sym if ldap_config[Rails.env]["ssl"]
58
+
59
+ # LDAP server to connect to depending on the current Rails environment in use
60
+ @ldap = Net::LDAP.new(ldap_options)
61
+ @ldap.host = ldap_config[Rails.env]["host"]
62
+ @ldap.port = ldap_config[Rails.env]["port"]
63
+ @ldap.base = ldap_config[Rails.env]["base"]
64
+ @attribute = ldap_config[Rails.env]["attribute"]
65
+
66
+ # Admin credentials to use if an admin is set to bind to LDAP (Rails environment dependent)
67
+ @ldap.auth ldap_config[Rails.env]["admin_user"], ldap_config[Rails.env]["admin_password"] if @use_admin_to_bind
68
+
69
+ end
70
+
71
+ # Logs a message to the console and the environment log file
72
+ def log (message)
73
+ DeviseLdapMultiple::Logger.send(message) if @logging_enabled
74
+ end
75
+
76
+ def password_updatable?
77
+ @update_passwords
78
+ end
79
+
80
+ def user_creatable?
81
+ @create_user
82
+ end
83
+
84
+ # Deletes a parameter from LDAP for an account
85
+ def delete_param(param)
86
+ update_ldap [[:delete, param.to_sym, nil]]
87
+ end
88
+
89
+ def set_param(param, new_value, is_password = false)
90
+ new_value = @auth_password_builder .call(new_value) if is_password
91
+ update_ldap( { param.to_sym => new_value } )
92
+ end
93
+
94
+ # Returns the DistinguishedName for an account, regardless of if it exists or not (calls a proc to determine the dn if it doesn't exists - proc defined in the config file)
95
+ def dn
96
+ @dn ||= begin
97
+ log("LDAP dn lookup: #{@attribute}=#{@login}")
98
+ log("LDAP dn lookup asdf: #{@attribute}=#{@login}")
99
+ ldap_entry = search_for_login
100
+ if ldap_entry.nil?
101
+ @auth_username_builder .call(@attribute,@login,@ldap)
102
+ else
103
+ ldap_entry.dn
104
+ end
105
+ end
106
+ end
107
+
108
+ def ldap_param_value(param)
109
+ ldap_entry = search_for_login
110
+
111
+ if ldap_entry
112
+ unless ldap_entry[param].empty?
113
+ value = ldap_entry.send(param)
114
+ log("Requested param #{param} has value #{value}")
115
+ value
116
+ else
117
+ log("Requested param #{param} does not exist")
118
+ value = nil
119
+ end
120
+ else
121
+ log("Requested ldap entry does not exist")
122
+ value = nil
123
+ end
124
+ end
125
+
126
+ def authenticate!
127
+ return false unless (@password.present? || @allow_unauthenticated_bind)
128
+ @ldap.auth(dn, @password)
129
+ @ldap.bind
130
+ end
131
+
132
+ def authenticated?
133
+ authenticate!
134
+ end
135
+
136
+ def last_message_bad_credentials?
137
+ @ldap.get_operation_result.error_message.to_s.include? 'AcceptSecurityContext error, data 52e'
138
+ end
139
+
140
+ def last_message_expired_credentials?
141
+ @ldap.get_operation_result.error_message.to_s.include? 'AcceptSecurityContext error, data 773'
142
+ end
143
+
144
+ def authorized?
145
+ log("Authorizing user #{dn}")
146
+ if !authenticated?
147
+ if last_message_bad_credentials?
148
+ log("Not authorized because of invalid credentials.")
149
+ elsif last_message_expired_credentials?
150
+ log("Not authorized because of expired credentials.")
151
+ else
152
+ log("Not authorized because not authenticated.")
153
+ end
154
+ return false
155
+ elsif !in_required_groups?
156
+ log("Not authorized because not in required groups.")
157
+ return false
158
+ elsif !has_required_attribute?
159
+ log("Not authorized because does not have required attribute.")
160
+ return false
161
+ elsif !has_required_attribute_presence?
162
+ log("Not authorized because does not have required attribute present.")
163
+ return false
164
+ else
165
+ return true
166
+ end
167
+ end
168
+
169
+ def expired_valid_credentials?
170
+ log("Authorizing user #{dn}")
171
+ !authenticated? && last_message_expired_credentials?
172
+ end
173
+
174
+ def change_password!
175
+ update_ldap(:userPassword => @auth_password_builder .call(@new_password))
176
+ end
177
+
178
+ def in_required_groups?
179
+ return true unless @check_group_membership || @check_group_membership_without_admin
180
+ ## FIXME set errors here, the ldap.yml isn't set properly.
181
+ return false if @required_groups.nil?
182
+ for group in @required_groups
183
+ if group.is_a?(Array)
184
+ return false unless in_group?(group[1], group[0])
185
+ else
186
+ return false unless in_group?(group)
187
+ end
188
+ end
189
+ return true
190
+ end
191
+
192
+ def in_group?(group_name, group_attribute = LDAP::DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY)
193
+ in_group = false
194
+ if @check_group_membership_without_admin
195
+ group_checking_ldap = @ldap
196
+ else
197
+ group_checking_ldap = Connection.admin
198
+ end
199
+ unless @ldap_check_group_membership
200
+ group_checking_ldap.search(:base => group_name, :scope => Net::LDAP::SearchScope_BaseObject) do |entry|
201
+ if entry[group_attribute].include? dn
202
+ in_group = true
203
+ log("User #{dn} IS included in group: #{group_name}")
204
+ end
205
+ end
206
+ else
207
+ # AD optimization - extension will recursively check sub-groups with one query
208
+ # "(memberof:1.2.840.113556.1.4.1941:=group_name)"
209
+ search_result = group_checking_ldap.search(:base => dn,
210
+ :filter => Net::LDAP::Filter.ex("memberof:1.2.840.113556.1.4.1941", group_name),
211
+ :scope => Net::LDAP::SearchScope_BaseObject)
212
+ # Will return the user entry if belongs to group otherwise nothing
213
+ if search_result.length == 1 && search_result[0].dn.eql?(dn)
214
+ in_group = true
215
+ log("User #{dn} IS included in group: #{group_name}")
216
+ end
217
+ end
218
+
219
+ unless in_group
220
+ log("User #{dn} is not in group: #{group_name}")
221
+ end
222
+
223
+ return in_group
224
+ end
225
+
226
+ def has_required_attribute?
227
+ return true unless @check_attributes
228
+ admin_ldap = Connection.admin
229
+ user = find_ldap_user(admin_ldap)
230
+ @required_attributes.each do |key,val|
231
+ matching_attributes = user[key] & Array(val)
232
+ unless (matching_attributes).any?
233
+ log("User #{dn} did not match attribute #{key}:#{val}")
234
+ return false
235
+ end
236
+ end
237
+ return true
238
+ end
239
+
240
+ def has_required_attribute_presence?
241
+ return true unless @check_attributes_presence
242
+ user = search_for_login
243
+ @required_attributes_presence.each do |key,val|
244
+ if val && !user.attribute_names.include?(key.to_sym)
245
+ log("User #{dn} doesn't include attribute #{key}")
246
+ return false
247
+ elsif !val && user.attribute_names.include?(key.to_sym)
248
+ log("User #{dn} includes attribute #{key}")
249
+ return false
250
+ end
251
+ end
252
+ return true
253
+ end
254
+
255
+ def user_groups
256
+ admin_ldap = Connection.admin
257
+ log("Getting groups for #{dn}")
258
+ filter = Net::LDAP::Filter.eq(@group_membership_attribute, dn)
259
+ admin_ldap.search(:filter => filter, :base => @group_base).collect(&:dn)
260
+ end
261
+ def valid_login?
262
+ !search_for_login.nil?
263
+ end
264
+
265
+ # Searches the LDAP for the login
266
+ #
267
+ # @return [Object] the LDAP entry found; nil if not found
268
+ def search_for_login
269
+ @login_ldap_entry ||= begin
270
+ log("LDAP search for login: #{@attribute}=#{@login}")
271
+ log("Attribute: #{@attribute.to_s}, Login: #{@login.to_s}")
272
+ log("Scope is: #{@scope}")
273
+ filter = Net::LDAP::Filter.eq(@attribute.to_s, @login.to_s)
274
+ ldap_entry = nil
275
+ match_count = 0
276
+ @ldap.search(:filter => filter) { |entry| ldap_entry = entry; match_count+=1}
277
+ op_result= @ldap.get_operation_result
278
+ if op_result.code!=0 then
279
+ log("LDAP Error #{op_result.code}: #{op_result.message}")
280
+ end
281
+ log("LDAP search yielded #{match_count} matches")
282
+ ldap_entry
283
+ end
284
+ end
285
+
286
+ private
287
+
288
+ def self.admin
289
+ ldap = Connection.new(:admin => true).ldap
290
+ unless ldap.bind
291
+ log("Cannot bind to admin LDAP user")
292
+ raise DeviseLdapAuthenticatable::LdapException, "Cannot connect to admin LDAP user"
293
+ end
294
+
295
+ return ldap
296
+ end
297
+
298
+ def find_ldap_user(ldap)
299
+ log("Finding user: #{dn}")
300
+ ldap.search(:base => dn, :scope => Net::LDAP::SearchScope_BaseObject).try(:first)
301
+ end
302
+
303
+ def update_ldap(ops)
304
+ operations = []
305
+ if ops.is_a? Hash
306
+ ops.each do |key,value|
307
+ operations << [:replace,key,value]
308
+ end
309
+ elsif ops.is_a? Array
310
+ operations = ops
311
+ end
312
+ if @use_admin_to_bind
313
+ privileged_ldap = Connection.admin
314
+ else
315
+ authenticate!
316
+ privileged_ldap = self.ldap
317
+ end
318
+ log("Modifying user #{dn}")
319
+ privileged_ldap.modify(:dn => dn, :operations => operations)
320
+ end
321
+
322
+ end # class Connection
323
+
324
+ end # module LDAP
325
+
326
+ end # module Devise
@@ -0,0 +1,9 @@
1
+ module DeviseLdapMultiple
2
+
3
+ class Logger
4
+ def self.send(message, logger = Rails.logger)
5
+ logger.add 0, " \e[36mLDAP:\e[0m #{message}"
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,131 @@
1
+ require 'devise_ldap_multiple/strategy'
2
+
3
+ module Devise
4
+
5
+ module Models
6
+
7
+ # LDAP Module, responsible for validating the user credentials via LDAP.
8
+ #
9
+ # Examples:
10
+ #
11
+ # User.authenticate('email@test.com', 'password123') # returns authenticated user or nil
12
+ # User.find(1).valid_password?('password123') # returns true/false
13
+ #
14
+ module LdapAuthenticatable
15
+
16
+ extend ActiveSupport::Concern
17
+
18
+ # Returns the current scope / mapping from devise (eg: 'admin', 'user', 'staff')
19
+ def current_scope
20
+ Devise.mappings.find {|k,v| v.class_name == self.class.name}.last.name.downcase
21
+ end
22
+
23
+ included do
24
+ attr_reader :current_password, :password
25
+ attr_accessor :password_confirmation
26
+ end
27
+
28
+ def login_with
29
+ @login_with ||= Devise.mappings.find {|k,v| v.class_name == self.class.name}.last.to.authentication_keys.first
30
+ self[@login_with]
31
+ end
32
+
33
+ def change_password!(current_password)
34
+ raise "Need to set new password first" if @password.blank?
35
+ Devise::LDAP::Adapter.update_own_password(login_with, @password, current_password, current_scope)
36
+ end
37
+
38
+ def reset_password!(new_password, new_password_confirmation)
39
+ if new_password == new_password_confirmation && Devise::LDAP::Adapter.password_updatable?(login_with, current_scope)
40
+ Devise::LDAP::Adapter.update_password(login_with, new_password, current_scope)
41
+ end
42
+ clear_reset_password_token if valid?
43
+ save
44
+ end
45
+
46
+ def password=(new_password)
47
+ @password = new_password
48
+ if defined?(password_digest) && @password.present? && respond_to?(:encrypted_password=)
49
+ self.encrypted_password = password_digest(@password)
50
+ end
51
+ end
52
+
53
+ # Checks if a resource is valid upon authentication.
54
+ def valid_ldap_authentication?(password)
55
+ Devise::LDAP::Adapter.valid_credentials?(login_with, password, current_scope)
56
+ end
57
+
58
+ def ldap_entry
59
+ @ldap_entry ||= Devise::LDAP::Adapter.get_ldap_entry(login_with, current_scope)
60
+ end
61
+
62
+ def ldap_groups
63
+ Devise::LDAP::Adapter.get_groups(current_scope, login_with)
64
+ end
65
+
66
+ def in_ldap_group?(group_name, group_attribute = LDAP::DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY)
67
+ Devise::LDAP::Adapter.in_ldap_group?(login_with, group_name, group_attribute, current_scope)
68
+ end
69
+
70
+ def ldap_dn
71
+ ldap_entry ? ldap_entry.dn : nil
72
+ end
73
+
74
+ def ldap_get_param(param)
75
+ if ldap_entry && !ldap_entry[param].empty?
76
+ value = ldap_entry.send(param)
77
+ else
78
+ nil
79
+ end
80
+ end
81
+
82
+ #
83
+ # callbacks
84
+ #
85
+
86
+ # # Called before the ldap record is saved automatically
87
+ # def ldap_before_save
88
+ # end
89
+
90
+ # Called after a successful LDAP authentication
91
+ def after_ldap_authentication
92
+ end
93
+
94
+
95
+ module ClassMethods
96
+
97
+ # Find a user for ldap authentication.
98
+ def find_for_ldap_authentication(attributes={})
99
+ auth_key = self.authentication_keys.first
100
+ return nil unless attributes[auth_key].present?
101
+
102
+ auth_key_value = (self.case_insensitive_keys || []).include?(auth_key) ? attributes[auth_key].downcase : attributes[auth_key]
103
+ auth_key_value = (self.strip_whitespace_keys || []).include?(auth_key) ? auth_key_value.strip : auth_key_value
104
+
105
+ resource = where(auth_key => auth_key_value).first
106
+
107
+ if resource.blank?
108
+ resource = new
109
+ resource[auth_key] = auth_key_value
110
+ resource.password = attributes[:password]
111
+ end
112
+
113
+ if Devise::LDAP::Adapter.user_creatable?(self.authentication_keys.first, self.name.downcase) && resource.new_record? && resource.valid_ldap_authentication?(attributes[:password])
114
+ resource.ldap_before_save if resource.respond_to?(:ldap_before_save)
115
+ resource.save!
116
+ end
117
+
118
+ resource
119
+ end
120
+
121
+ def update_with_password(resource)
122
+ puts "UPDATE_WITH_PASSWORD: #{resource.inspect}"
123
+ end
124
+
125
+ end # module ClassMethods
126
+
127
+ end # module LdapAuthenticatable
128
+
129
+ end # module Models
130
+
131
+ end # module Devise
@@ -0,0 +1,48 @@
1
+ require 'devise/strategies/authenticatable'
2
+
3
+ module Devise
4
+
5
+ module Strategies
6
+
7
+ # Defines an authentication strategy to be used with Warden
8
+ # Some more information about this here: http://www.rubydoc.info/github/hassox/warden/Warden/Strategies/Base
9
+ class LdapAuthenticatable < Authenticatable
10
+
11
+ # Tests whether the returned resource exists in the database and the
12
+ # credentials are valid. If the resource is in the database and the credentials
13
+ # are valid, the user is authenticated. Otherwise failure messages are returned
14
+ # indicating whether the resource is not found in the database or the credentials
15
+ # are invalid.
16
+ def authenticate!
17
+
18
+ resource = mapping.to.find_for_ldap_authentication(authentication_hash.merge(:password => password))
19
+ return fail(:invalid) unless resource
20
+
21
+ if resource.persisted?
22
+ if validate(resource) { resource.valid_ldap_authentication?(password) }
23
+ remember_me(resource)
24
+ resource.after_ldap_authentication
25
+ success!(resource)
26
+ else
27
+ return fail(:invalid) # Invalid credentials
28
+ end
29
+ end
30
+
31
+ if resource.new_record?
32
+ if validate(resource) { resource.valid_ldap_authentication?(password) }
33
+ return fail(:not_found_in_database) # Valid credentials
34
+ else
35
+ return fail(:invalid) # Invalid credentials
36
+ end
37
+ end
38
+
39
+ end # def authenticate!
40
+
41
+ end # LdapAuthenticatable < Authenticatable
42
+
43
+ end # module Strategies
44
+
45
+ end # module Devise
46
+
47
+ # Add the strategy to warden
48
+ Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
@@ -0,0 +1,3 @@
1
+ module DeviseLdapMultiple
2
+ VERSION = "0.9.0".freeze
3
+ end
@@ -0,0 +1,17 @@
1
+
2
+ class DeviseLdapMultipleGenerator < Rails::Generators::Base
3
+
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ argument :user_model, :type => :string, :default => "user", :desc => "Model to update"
7
+
8
+ # ToDo: Request user input to use a scope that already exists, or make it a parameter to pass into the generator
9
+ def create_ldap_config
10
+ copy_file "default.yml", "config/ldap/#{user_model}.yml"
11
+ end
12
+
13
+ def update_user_model
14
+ gsub_file "app/models/#{options.user_model}.rb", /:database_authenticatable/, ":ldap_authenticatable" if options.update_model?
15
+ end
16
+
17
+ end
@@ -0,0 +1,39 @@
1
+ module DeviseLdapMultiple
2
+
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ source_root File.expand_path("../templates", __FILE__)
6
+
7
+ # ToDo: Request user input to use a scope that already exists, or make it a parameter to pass into the generator
8
+ def create_ldap_config
9
+ copy_file "default.yml", "config/ldap/default.yml"
10
+ end
11
+
12
+ def create_default_devise_settings
13
+ inject_into_file "config/initializers/devise.rb", default_devise_settings, :after => "Devise.setup do |config|\n"
14
+ end
15
+
16
+ def update_application_controller
17
+ inject_into_class "app/controllers/application_controller.rb", ApplicationController, rescue_from_exception if options.add_rescue?
18
+ end
19
+
20
+ private
21
+
22
+ def default_devise_settings
23
+ settings = <<-eof
24
+ # ==> Devise LDAP Multiple configuration
25
+ config.ldap_default_scope = 'default'
26
+ eof
27
+ settings
28
+ end
29
+
30
+ def rescue_from_exception
31
+ <<-eof
32
+ rescue_from DeviseLdapMultiple::LdapException do |exception|
33
+ render :text => exception, :status => 500
34
+ end
35
+ eof
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,99 @@
1
+ ### Configuration options ###
2
+
3
+ # (These used to reside in config/initializers/devise.rb)
4
+
5
+ # If set to true, will log LDAP queries to the Rails logger
6
+ logging: true
7
+
8
+ # If set to true, all valid LDAP users will be allowed to login and an appropriate user record will be created.
9
+ # If set to false, you will have to create the user record before they will be allowed to login.
10
+ create_user: true
11
+
12
+ # When doing password resets, if true will update the LDAP server. Requires admin password.
13
+ update_passwords: true
14
+
15
+ # When set to true, the user trying to login will be checked to make sure they are in all of groups specified
16
+ check_group_membership: false
17
+
18
+ # When set to true, the user trying to login will be checked to make sure their attributes match those specified
19
+ check_attributes: false
20
+
21
+ # When set to true, the user trying to login will be checked against all require_attribute_presence attributes
22
+ check_attributes_presence: false
23
+
24
+ # When set to true, the admin user will be used to bind to the LDAP server during authentication.
25
+ use_admin_to_bind: true
26
+
27
+ # When set to true, the group membership check is done with the user's own credentials rather than with admin credentials.
28
+ # Since these credentials are only available to the Devise user model during the login flow, the group check function will
29
+ # not work if a group check is performed when this option is true outside of the login flow (e.g., before particular actions).
30
+ check_group_membership_without_admin: false
31
+
32
+ # You can pass a proc to the username option to explicitly specify the format that you search for a users' DN on your LDAP server.
33
+ auth_username_builder: Proc.new() {|attribute, login, ldap| "#{attribute}=#{login},#{ldap.base}" }
34
+
35
+ # Optionally you can define a proc to create custom password encrption when user reset password
36
+ auth_password_builder: Proc.new() {|new_password| Net::LDAP::Password.generate(:sha, new_password) }
37
+
38
+ ### Authorizations ###
39
+
40
+ # Uncomment out the merging for each environment that you'd like to include.
41
+ # You can also just copy and paste the tree (do not include the "authorizations") to each
42
+ # environment if you need something different per environment.
43
+
44
+ authorizations: &AUTHORIZATIONS
45
+ allow_unauthenticated_bind: false
46
+ group_base: ou=groups,dc=test,dc=com
47
+ # Requires check_group_membership to be true
48
+ # Can have multiple values, must match all to be authorized
49
+
50
+ required_groups:
51
+ # If only a group name is given, membership will be checked against "uniqueMember"
52
+ - cn=admins,ou=groups,dc=test,dc=com
53
+ - cn=users,ou=groups,dc=test,dc=com
54
+ # If an array is given, the first element will be the attribute to check against, the second the group name
55
+ - ["moreMembers", "cn=users,ou=groups,dc=test,dc=com"]
56
+ # Requires check_attributes to be true
57
+ # Can have multiple attributes and values, must match all to be authorized
58
+ require_attribute:
59
+ objectClass: inetOrgPerson
60
+ authorizationRole: postsAdmin
61
+
62
+ # Requires check_attributes_presence to be true
63
+ # Can have multiple attributes set to true or false to check presence, all must match all to be authorized
64
+ require_attribute_presence:
65
+ mail: true
66
+ telephoneNumber: true
67
+ serviceAccount: false
68
+
69
+ ### Environment ###
70
+
71
+ development:
72
+ host: localhost
73
+ port: 389
74
+ attribute: userPrincipalName
75
+ base: ou=people,dc=test,dc=com
76
+ admin_user: cn=admin,dc=test,dc=com
77
+ admin_password: admin_password
78
+ ssl: false
79
+ # <<: *AUTHORIZATIONS
80
+
81
+ test:
82
+ host: localhost
83
+ port: 3389
84
+ attribute: userPrincipalName
85
+ base: ou=people,dc=test,dc=com
86
+ admin_user: cn=admin,dc=test,dc=com
87
+ admin_password: admin_password
88
+ ssl: simple_tls
89
+ # <<: *AUTHORIZATIONS
90
+
91
+ production:
92
+ host: localhost
93
+ port: 636
94
+ attribute: userPrincipalName
95
+ base: ou=people,dc=test,dc=com
96
+ admin_user: cn=admin,dc=test,dc=com
97
+ admin_password: admin_password
98
+ ssl: start_tls
99
+ # <<: *AUTHORIZATIONS
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devise_ldap_multiple
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Curtis Schiewek
8
+ - Daniel McNevin
9
+ - Steven Xu
10
+ - Scott Willett
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2016-11-11 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: devise
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 3.4.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: net-ldap
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0.6'
37
+ - - "!="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.12'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.6'
47
+ - - "!="
48
+ - !ruby/object:Gem::Version
49
+ version: '0.12'
50
+ - !ruby/object:Gem::Dependency
51
+ name: rake
52
+ requirement: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0.9'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0.9'
64
+ - !ruby/object:Gem::Dependency
65
+ name: rdoc
66
+ requirement: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rails
80
+ requirement: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '4.0'
85
+ type: :development
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '4.0'
92
+ - !ruby/object:Gem::Dependency
93
+ name: sqlite3
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ - !ruby/object:Gem::Dependency
107
+ name: factory_girl_rails
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '1.0'
113
+ type: :development
114
+ prerelease: false
115
+ version_requirements: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.0'
120
+ - !ruby/object:Gem::Dependency
121
+ name: factory_girl
122
+ requirement: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '2.0'
127
+ type: :development
128
+ prerelease: false
129
+ version_requirements: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '2.0'
134
+ - !ruby/object:Gem::Dependency
135
+ name: rspec-rails
136
+ requirement: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ type: :development
142
+ prerelease: false
143
+ version_requirements: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ - !ruby/object:Gem::Dependency
149
+ name: database_cleaner
150
+ requirement: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ type: :development
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ - !ruby/object:Gem::Dependency
163
+ name: capybara
164
+ requirement: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ type: :development
170
+ prerelease: false
171
+ version_requirements: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ - !ruby/object:Gem::Dependency
177
+ name: launchy
178
+ requirement: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ type: :development
184
+ prerelease: false
185
+ version_requirements: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ description: Devise extension to allow authentication to multiple LDAPs. Fork of the
191
+ devise_ldap_authenticatable project.
192
+ email: swillett@outlook.com
193
+ executables: []
194
+ extensions: []
195
+ extra_rdoc_files: []
196
+ files:
197
+ - ".gitignore"
198
+ - Gemfile
199
+ - MIT-LICENSE
200
+ - README.md
201
+ - Rakefile
202
+ - devise_ldap_multiple.gemspec
203
+ - lib/devise_ldap_multiple.rb
204
+ - lib/devise_ldap_multiple/exception.rb
205
+ - lib/devise_ldap_multiple/ldap/adapter.rb
206
+ - lib/devise_ldap_multiple/ldap/connection.rb
207
+ - lib/devise_ldap_multiple/logger.rb
208
+ - lib/devise_ldap_multiple/model.rb
209
+ - lib/devise_ldap_multiple/strategy.rb
210
+ - lib/devise_ldap_multiple/version.rb
211
+ - lib/generators/devise_ldap_multiple/devise_ldap_multiple_generator.rb
212
+ - lib/generators/devise_ldap_multiple/install_generator.rb
213
+ - lib/generators/devise_ldap_multiple/templates/default.yml
214
+ homepage: https://github.com/xarael/devise_ldap_multiple
215
+ licenses:
216
+ - MIT
217
+ metadata: {}
218
+ post_install_message:
219
+ rdoc_options: []
220
+ require_paths:
221
+ - lib
222
+ required_ruby_version: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - ">="
225
+ - !ruby/object:Gem::Version
226
+ version: '0'
227
+ required_rubygems_version: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - ">="
230
+ - !ruby/object:Gem::Version
231
+ version: '0'
232
+ requirements: []
233
+ rubyforge_project:
234
+ rubygems_version: 2.5.1
235
+ signing_key:
236
+ specification_version: 4
237
+ summary: Devise extension to allow authentication to multiple LDAPs. Fork of the devise_ldap_authenticatable
238
+ project.
239
+ test_files: []