devise_ldap_multiple 0.9.0

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