omniauth-ldap 2.0.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/REEK ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
@@ -1,59 +1,130 @@
1
- require 'omniauth'
1
+ require "omniauth"
2
+ require "omniauth/version"
2
3
 
3
4
  module OmniAuth
4
5
  module Strategies
5
6
  class LDAP
7
+ OMNIAUTH_GTE_V2 = Gem::Version.new(OmniAuth::VERSION) >= Gem::Version.new("2.0.0")
6
8
  include OmniAuth::Strategy
7
- @@config = {
8
- 'name' => 'cn',
9
- 'first_name' => 'givenName',
10
- 'last_name' => 'sn',
11
- 'email' => ['mail', "email", 'userPrincipalName'],
12
- 'phone' => ['telephoneNumber', 'homePhone', 'facsimileTelephoneNumber'],
13
- 'mobile' => ['mobile', 'mobileTelephoneNumber'],
14
- 'nickname' => ['uid', 'userid', 'sAMAccountName'],
15
- 'title' => 'title',
16
- 'location' => {"%0, %1, %2, %3 %4" => [['address', 'postalAddress', 'homePostalAddress', 'street', 'streetAddress'], ['l'], ['st'],['co'],['postOfficeBox']]},
17
- 'uid' => 'dn',
18
- 'url' => ['wwwhomepage'],
19
- 'image' => 'jpegPhoto',
20
- 'description' => 'description'
9
+
10
+ InvalidCredentialsError = Class.new(StandardError)
11
+
12
+ option :mapping, {
13
+ "name" => "cn",
14
+ "first_name" => "givenName",
15
+ "last_name" => "sn",
16
+ "email" => ["mail", "email", "userPrincipalName"],
17
+ "phone" => ["telephoneNumber", "homePhone", "facsimileTelephoneNumber"],
18
+ "mobile" => ["mobile", "mobileTelephoneNumber"],
19
+ "nickname" => ["uid", "userid", "sAMAccountName"],
20
+ "title" => "title",
21
+ "location" => {"%0, %1, %2, %3 %4" => [["address", "postalAddress", "homePostalAddress", "street", "streetAddress"], ["l"], ["st"], ["co"], ["postOfficeBox"]]},
22
+ "uid" => "dn",
23
+ "url" => ["wwwhomepage"],
24
+ "image" => "jpegPhoto",
25
+ "description" => "description",
21
26
  }
22
- option :title, "LDAP Authentication" #default title for authentication form
27
+ option :title, "LDAP Authentication" # default title for authentication form
28
+ # For OmniAuth >= 2.0 the default allowed request method is POST only.
29
+ # Ensure the strategy follows that default so GET /auth/:provider returns 404 as expected in tests.
30
+ if OMNIAUTH_GTE_V2
31
+ option(:request_methods, [:post])
32
+ else
33
+ option(:request_methods, [:get, :post])
34
+ end
23
35
  option :port, 389
24
36
  option :method, :plain
25
- option :uid, 'sAMAccountName'
26
- option :name_proc, lambda {|n| n}
37
+ option :disable_verify_certificates, false
38
+ option :ca_file, nil
39
+ option :ssl_version, nil # use OpenSSL default if nil
40
+ option :uid, "sAMAccountName"
41
+ option :name_proc, lambda { |n| n }
42
+ # Trusted header SSO support (disabled by default)
43
+ # :header_auth - when true and the header is present, the strategy trusts the upstream gateway
44
+ # and searches the directory for the user without requiring a user password.
45
+ # :header_name - which header/env key to read (default: "REMOTE_USER"). We will also check the
46
+ # standard Rack "HTTP_" variant automatically.
47
+ option :header_auth, false
48
+ option :header_name, "REMOTE_USER"
49
+ # Optional timeouts (forwarded to Net::LDAP when supported)
50
+ option :connect_timeout, nil
51
+ option :read_timeout, nil
27
52
 
28
53
  def request_phase
29
- OmniAuth::LDAP::Adaptor.validate @options
30
- f = OmniAuth::Form.new(:title => (options[:title] || "LDAP Authentication"), :url => callback_path)
31
- f.text_field 'Login', 'username'
32
- f.password_field 'Password', 'password'
33
- f.button "Sign In"
54
+ # OmniAuth >= 2.0 expects the request phase to be POST-only for /auth/:provider.
55
+ # Some test environments (and OmniAuth itself) enforce this by returning 404 on GET.
56
+ if OMNIAUTH_GTE_V2 && request.get?
57
+ return Rack::Response.new("", 404, {"Content-Type" => "text/plain"}).finish
58
+ end
59
+
60
+ # Fast-path: if a trusted identity header is present, skip the login form
61
+ # and jump to the callback where we will complete using directory lookup.
62
+ if header_username
63
+ return Rack::Response.new([], 302, "Location" => callback_url).finish
64
+ end
65
+
66
+ # If credentials were POSTed directly to /auth/:provider, redirect to the callback path.
67
+ # This mirrors the behavior of many OmniAuth providers and allows test helpers (like
68
+ # OmniAuth::Test::PhonySession) to populate `env['omniauth.auth']` on the callback request.
69
+ if request.post? && request_data["username"].to_s != "" && request_data["password"].to_s != ""
70
+ return Rack::Response.new([], 302, "Location" => callback_url).finish
71
+ end
72
+
73
+ OmniAuth::LDAP::Adaptor.validate(@options)
74
+ f = OmniAuth::Form.new(title: options[:title] || "LDAP Authentication", url: callback_url)
75
+ f.text_field("Login", "username")
76
+ f.password_field("Password", "password")
77
+ f.button("Sign In")
34
78
  f.to_response
35
79
  end
36
80
 
37
81
  def callback_phase
38
- @adaptor = OmniAuth::LDAP::Adaptor.new @options
82
+ @adaptor = OmniAuth::LDAP::Adaptor.new(@options)
83
+
84
+ return fail!(:invalid_request_method) unless valid_request_method?
85
+
86
+ # Header-based SSO (REMOTE_USER-style) path
87
+ if (hu = header_username)
88
+ begin
89
+ entry = directory_lookup(@adaptor, hu)
90
+ unless entry
91
+ return fail!(:invalid_credentials, InvalidCredentialsError.new("User not found for header #{hu}"))
92
+ end
93
+ @ldap_user_info = entry
94
+ @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
95
+ return super
96
+ rescue => e
97
+ return fail!(:ldap_error, e)
98
+ end
99
+ end
39
100
 
40
101
  return fail!(:missing_credentials) if missing_credentials?
41
102
  begin
42
- @ldap_user_info = @adaptor.bind_as(:filter => filter(@adaptor), :size => 1, :password => request['password'])
43
- return fail!(:invalid_credentials) if !@ldap_user_info
103
+ @ldap_user_info = @adaptor.bind_as(filter: filter(@adaptor), size: 1, password: request_data["password"])
104
+
105
+ unless @ldap_user_info
106
+ # Attach password policy info to env if available (best-effort)
107
+ attach_password_policy_env(@adaptor)
108
+ return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request_data["username"]}"))
109
+ end
44
110
 
45
- @user_info = self.class.map_user(@@config, @ldap_user_info)
111
+ # Optionally attach policy info even on success (e.g., timeBeforeExpiration)
112
+ attach_password_policy_env(@adaptor)
113
+
114
+ @user_info = self.class.map_user(@options[:mapping], @ldap_user_info)
46
115
  super
47
- rescue Exception => e
48
- return fail!(:ldap_error, e)
116
+ rescue => e
117
+ fail!(:ldap_error, e)
49
118
  end
50
119
  end
51
120
 
52
- def filter adaptor
53
- if adaptor.filter and !adaptor.filter.empty?
54
- Net::LDAP::Filter.construct(adaptor.filter % {username: @options[:name_proc].call(request['username'])})
121
+ def filter(adaptor, username_override = nil)
122
+ flt = adaptor.filter
123
+ if flt && !flt.to_s.empty?
124
+ username = Net::LDAP::Filter.escape(@options[:name_proc].call(username_override || request_data["username"]))
125
+ Net::LDAP::Filter.construct(flt % {username: username})
55
126
  else
56
- Net::LDAP::Filter.eq(adaptor.uid, @options[:name_proc].call(request['username']))
127
+ Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(username_override || request_data["username"]))
57
128
  end
58
129
  end
59
130
 
@@ -64,38 +135,127 @@ module OmniAuth
64
135
  @user_info
65
136
  }
66
137
  extra {
67
- { :raw_info => @ldap_user_info }
138
+ {raw_info: @ldap_user_info}
68
139
  }
69
140
 
70
- def self.map_user(mapper, object)
71
- user = {}
72
- mapper.each do |key, value|
73
- case value
74
- when String
75
- user[key] = object[value.downcase.to_sym].first if object.respond_to? value.downcase.to_sym
76
- when Array
77
- value.each {|v| (user[key] = object[v.downcase.to_sym].first; break;) if object.respond_to? v.downcase.to_sym}
78
- when Hash
79
- value.map do |key1, value1|
80
- pattern = key1.dup
81
- value1.each_with_index do |v,i|
82
- part = ''; v.collect(&:downcase).collect(&:to_sym).each {|v1| (part = object[v1].first; break;) if object.respond_to? v1}
83
- pattern.gsub!("%#{i}",part||'')
141
+ class << self
142
+ def map_user(mapper, object)
143
+ user = {}
144
+ mapper.each do |key, value|
145
+ case value
146
+ when String
147
+ user[key] = object[value.downcase.to_sym].first if object.respond_to?(value.downcase.to_sym)
148
+ when Array
149
+ value.each do |v|
150
+ if object.respond_to?(v.downcase.to_sym)
151
+ user[key] = object[v.downcase.to_sym].first
152
+ break
153
+ end
154
+ end
155
+ when Hash
156
+ value.map do |key1, value1|
157
+ pattern = key1.dup
158
+ value1.each_with_index do |v, i|
159
+ part = ""
160
+ v.collect(&:downcase).collect(&:to_sym).each do |v1|
161
+ if object.respond_to?(v1)
162
+ part = object[v1].first
163
+ break
164
+ end
165
+ end
166
+ pattern.gsub!("%#{i}", part || "")
167
+ end
168
+ user[key] = pattern
84
169
  end
85
- user[key] = pattern
170
+ else
171
+ # unknown mapping type; ignore
86
172
  end
87
173
  end
174
+ user
88
175
  end
89
- user
90
176
  end
91
177
 
92
178
  protected
93
179
 
180
+ def valid_request_method?
181
+ request.env["REQUEST_METHOD"] == "POST"
182
+ end
183
+
94
184
  def missing_credentials?
95
- request['username'].nil? or request['username'].empty? or request['password'].nil? or request['password'].empty?
96
- end # missing_credentials?
185
+ request_data["username"].nil? || request_data["username"].empty? || request_data["password"].nil? || request_data["password"].empty?
186
+ end
187
+
188
+ def request_data
189
+ @env["action_dispatch.request.request_parameters"] || request.params
190
+ end
191
+
192
+ # Extract a normalized username from a trusted header when enabled.
193
+ # Returns nil when not configured or not present.
194
+ def header_username
195
+ return unless options[:header_auth]
196
+
197
+ name = options[:header_name] || "REMOTE_USER"
198
+ # Try both the raw env var (e.g., REMOTE_USER) and the Rack HTTP_ variant (e.g., HTTP_REMOTE_USER or HTTP_X_REMOTE_USER)
199
+ raw = request.env[name] || request.env["HTTP_#{name.upcase.tr("-", "_")}"]
200
+ return if raw.nil? || raw.to_s.strip.empty?
201
+
202
+ options[:name_proc].call(raw.to_s)
203
+ end
204
+
205
+ # Perform a directory lookup for the given username using the strategy configuration
206
+ # (bind_dn/password or anonymous). Does not attempt to bind as the user.
207
+ def directory_lookup(adaptor, username)
208
+ entry = nil
209
+ search_filter = filter(adaptor, username)
210
+ adaptor.connection.open do |conn|
211
+ rs = conn.search(filter: search_filter, size: 1)
212
+ entry = rs && rs.first
213
+ end
214
+ entry
215
+ end
216
+
217
+ # If the adaptor captured a Password Policy response control, expose a minimal, stable hash
218
+ # in the Rack env for applications to inspect.
219
+ def attach_password_policy_env(adaptor)
220
+ return unless adaptor.respond_to?(:password_policy) && adaptor.password_policy
221
+ ctrl = adaptor.respond_to?(:last_password_policy_response) ? adaptor.last_password_policy_response : nil
222
+ op = adaptor.respond_to?(:last_operation_result) ? adaptor.last_operation_result : nil
223
+ return unless ctrl || op
224
+
225
+ request.env["omniauth.ldap.password_policy"] = extract_password_policy(ctrl, op)
226
+ end
227
+
228
+ # Best-effort extraction across net-ldap versions; if fields are not available, returns a raw payload.
229
+ def extract_password_policy(control, operation)
230
+ data = {raw: control}
231
+ if control
232
+ # Prefer named readers if present
233
+ if control.respond_to?(:error)
234
+ data[:error] = control.public_send(:error)
235
+ elsif control.respond_to?(:ppolicy_error)
236
+ data[:error] = control.public_send(:ppolicy_error)
237
+ end
238
+ if control.respond_to?(:time_before_expiration)
239
+ data[:time_before_expiration] = control.public_send(:time_before_expiration)
240
+ end
241
+ if control.respond_to?(:grace_authns_remaining)
242
+ data[:grace_authns_remaining] = control.public_send(:grace_authns_remaining)
243
+ elsif control.respond_to?(:grace_logins_remaining)
244
+ data[:grace_authns_remaining] = control.public_send(:grace_logins_remaining)
245
+ end
246
+ if control.respond_to?(:oid)
247
+ data[:oid] = control.public_send(:oid)
248
+ end
249
+ end
250
+ if operation
251
+ code = operation.respond_to?(:code) ? operation.code : nil
252
+ message = operation.respond_to?(:message) ? operation.message : nil
253
+ data[:operation] = {code: code, message: message}
254
+ end
255
+ data
256
+ end
97
257
  end
98
258
  end
99
259
  end
100
260
 
101
- OmniAuth.config.add_camelization 'ldap', 'LDAP'
261
+ OmniAuth.config.add_camelization("ldap", "LDAP")