veri 1.1.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5059569cdd9359f72eb4852779102dca282a75176073020888ecfffb6ac716c0
4
- data.tar.gz: ee0b24a4e5a9f105f10c6cb7cb550f90449a87031622577cc89211edac5bb06d
3
+ metadata.gz: 6bd0d2ab55db163c3fe4c23ffde6b23cd161d97c28c3fba95be4f7f88afbd2ea
4
+ data.tar.gz: b1afab87840a02696726deb462bff8e53c881e27ac78adf2787ba97cc59ae7ab
5
5
  SHA512:
6
- metadata.gz: '0910cd4e2a58796521a755ed2e6414d647270ce94582c2247574f2189e2949d5cb9297f0cbda06414586f845bb00a484974a2752e079de7906c770cd153afd20'
7
- data.tar.gz: 6bf989a229323abf6160b91bded464993f8606810d5883a40d32eb7e6e66c0bb4742cae90cf5246b1848f2b20a9014d66cf0867dea20de1dc10dbe297f1e1c5e
6
+ metadata.gz: 99b2f7cc063ebffc0fd94b96fcade3ea4d9e911f21a854146b1b09500319a693dc12d44e4bcb8d64a1382d10d33c8bae4df5321bb22fcc074f0afc9c9ca0f6bd
7
+ data.tar.gz: 2d4cc974fb4dacee177d526099d8524cd24b1b8733115a277b1b172fb84bf3e32b42a56558b57ab4552139f6c5364351708304d806cd6eb4eca22aa5497339c5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## v2.0.0
2
+
3
+ ### Breaking
4
+
5
+ - Changed `Veri::Session#shapeshift` method signature to accept an optional `tenant:` keyword argument
6
+ - `Veri::Session.prune` now deletes only sessions with orphaned tenants and no longer removes inactive or expired sessions
7
+
8
+ ### Features
9
+
10
+ - Added `Veri::Session#true_tenant` method to fetch the original tenant of a session
11
+
12
+ ### Bugs
13
+
14
+ - Fixed issue with user impersonation across different tenants
15
+
16
+ ### Misc
17
+
18
+ - Dropped some unnecessary dependencies
19
+
20
+ To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/enjaku4/veri/discussions/14)
21
+
1
22
  ## v1.1.0
2
23
 
3
24
  ### Features
data/README.md CHANGED
@@ -5,11 +5,13 @@
5
5
  [![Github Actions badge](https://github.com/enjaku4/veri/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/veri/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/github/license/enjaku4/veri.svg)](LICENSE)
7
7
 
8
- Veri is a cookie-based authentication library for Ruby on Rails. Unlike other solutions that generate controllers, views, and mailers for you, Veri provides only essential building blocks. It's ideal for applications that require custom authentication experiences: you design your own interfaces and flows, while Veri handles the complex underlying mechanics of secure password storage and session verification. On top of that, Veri supports multi-tenancy, granular session management, multiple password hashing algorithms, and includes a user impersonation feature.
8
+ Veri is a cookie-based authentication library for Ruby on Rails. It provides only essential building blocks for secure user authentication without cluttering your app with generated controllers, views, and mailers. This makes it ideal for building custom authentication flows.
9
+
10
+ Veri supports multi-tenancy, granular session management, multiple password hashing algorithms, and provides a user impersonation feature for administration purposes.
9
11
 
10
12
  **Example of Usage:**
11
13
 
12
- Consider a multi-tenant SaaS application where users can view all their active sessions across devices and browsers and terminate specific sessions remotely. Administrators have the same interface in their admin panel, giving them visibility into user activity and the ability to end sessions or lock accounts for security. Additionally, administrators can temporarily assume a user’s identity for troubleshooting. All of this is easily handled with Veri.
14
+ Consider a multi-tenant SaaS application where users need to manage their active sessions across devices and browsers, terminating specific sessions remotely when needed. Administrators require similar capabilities in their admin panel, with additional powers to lock accounts and temporarily assume user identities for troubleshooting. You can build all this easily with Veri.
13
15
 
14
16
  ## Table of Contents
15
17
 
@@ -20,6 +22,7 @@ Consider a multi-tenant SaaS application where users can view all their active s
20
22
  - [Controller Integration](#controller-integration)
21
23
  - [Authentication Sessions](#authentication-sessions)
22
24
  - [Account Lockout](#account-lockout)
25
+ - [User Impersonation](#user-impersonation)
23
26
  - [Multi-Tenancy](#multi-tenancy)
24
27
  - [View Helpers](#view-helpers)
25
28
  - [Testing](#testing)
@@ -39,11 +42,11 @@ gem "veri"
39
42
 
40
43
  Install the gem:
41
44
 
42
- ```bash
45
+ ```shell
43
46
  bundle install
44
47
  ```
45
48
 
46
- Generate the migration for your user model (replace `users` with your user table name if different):
49
+ Generate the migration for your user model (replace `users` with your table name if different):
47
50
 
48
51
  ```shell
49
52
  # For standard integer IDs
@@ -87,9 +90,9 @@ user.verify_password("password")
87
90
 
88
91
  ## Controller Integration
89
92
 
90
- ### Basic Setup
93
+ ### Setup
91
94
 
92
- Include the authentication module and configure protection:
95
+ Include the authentication module in your controllers and configure protection:
93
96
 
94
97
  ```rb
95
98
  class ApplicationController < ActionController::Base
@@ -103,6 +106,8 @@ class PicturesController < ApplicationController
103
106
  end
104
107
  ```
105
108
 
109
+ Both `with_authentication` and `skip_authentication` work exactly the same as Rails' `before_action` and `skip_before_action` methods.
110
+
106
111
  ### Authentication Methods
107
112
 
108
113
  This is a simplified example of how to use Veri's authentication methods:
@@ -168,54 +173,9 @@ return_path
168
173
  current_session
169
174
  ```
170
175
 
171
- ### User Impersonation (Shapeshifting)
172
-
173
- Veri provides user impersonation functionality that allows administrators to temporarily assume another user's identity:
174
-
175
- ```rb
176
- module Admin
177
- class ImpersonationController < ApplicationController
178
- def create
179
- user = User.find(params[:user_id])
180
- current_session.shapeshift(user)
181
- redirect_to root_path, notice: "Now viewing as #{user.name}"
182
- end
183
-
184
- def destroy
185
- original_user = current_session.true_identity
186
- current_session.to_true_identity
187
- redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
188
- end
189
- end
190
- end
191
- ```
192
-
193
- Available session methods:
194
-
195
- ```rb
196
- # Assume another user's identity (maintains original identity)
197
- session.shapeshift(user)
198
-
199
- # Return to original identity
200
- session.to_true_identity
201
-
202
- # Returns true if currently shapeshifted
203
- session.shapeshifted?
204
-
205
- # Returns original user when shapeshifted, otherwise current user
206
- session.true_identity
207
- ```
208
-
209
- Controller helper:
210
-
211
- ```rb
212
- # Returns true if the current session is shapeshifted
213
- shapeshifter?
214
- ```
215
-
216
176
  ### When Unauthenticated
217
177
 
218
- Override this private method to customize behavior for unauthenticated users:
178
+ By default, when unauthenticated, Veri redirects back (HTML) or returns 401 (other formats). Override this private method to customize behavior for unauthenticated users:
219
179
 
220
180
  ```rb
221
181
  class ApplicationController < ActionController::Base
@@ -223,17 +183,16 @@ class ApplicationController < ActionController::Base
223
183
 
224
184
  with_authentication
225
185
 
226
- # ...
227
-
228
186
  private
229
187
 
230
188
  def when_unauthenticated
231
- # By default, redirects back (HTML) or returns 401 (other formats)
232
189
  redirect_to login_path
233
190
  end
234
191
  end
235
192
  ```
236
193
 
194
+ The `when_unauthenticated` method can be overridden in any controller to provide controller-specific handling.
195
+
237
196
  ## Authentication Sessions
238
197
 
239
198
  Veri stores authentication sessions in the database, providing session management capabilities:
@@ -251,9 +210,10 @@ current_session
251
210
  ### Session Information
252
211
 
253
212
  ```rb
213
+ # Get the authenticated user
254
214
  session.identity
255
- # => authenticated user
256
215
 
216
+ # Get session details
257
217
  session.info
258
218
  # => {
259
219
  # device: "Desktop",
@@ -298,11 +258,11 @@ Veri::Session.terminate_all
298
258
  # Terminate all sessions for a specific user
299
259
  user.sessions.terminate_all
300
260
 
301
- # Clean up expired/inactive sessions, and sessions with deleted tenant
302
- Veri::Session.prune
261
+ # Clean up inactive sessions for a specific user
262
+ user.sessions.inactive.terminate_all
303
263
 
304
- # Clean up expired/inactive sessions for a specific user
305
- user.sessions.prune
264
+ # Clean up expired sessions globally
265
+ Veri::Session.expired.terminate_all
306
266
  ```
307
267
 
308
268
  ## Account Lockout
@@ -328,13 +288,58 @@ User.unlocked
328
288
 
329
289
  When an account is locked, the user cannot log in. If they're already logged in, their sessions are terminated and they are treated as unauthenticated.
330
290
 
291
+ ## User Impersonation
292
+
293
+ Veri provides user impersonation functionality that allows administrators to temporarily assume another user's identity:
294
+
295
+ ```rb
296
+ module Admin
297
+ class ImpersonationController < ApplicationController
298
+ def create
299
+ user = User.find(params[:user_id])
300
+ current_session.shapeshift(user)
301
+ redirect_to root_path, notice: "Now viewing as #{user.name}"
302
+ end
303
+
304
+ def destroy
305
+ original_user = current_session.true_identity
306
+ current_session.to_true_identity
307
+ redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
308
+ end
309
+ end
310
+ end
311
+ ```
312
+
313
+ Available session methods:
314
+
315
+ ```rb
316
+ # Assume another user's identity (in single-tenant applications)
317
+ session.shapeshift(user)
318
+
319
+ # Return to original identity
320
+ session.to_true_identity
321
+
322
+ # Returns true if currently shapeshifted
323
+ session.shapeshifted?
324
+
325
+ # Returns original user when shapeshifted, otherwise current user
326
+ session.true_identity
327
+ ```
328
+
329
+ Controller helper:
330
+
331
+ ```rb
332
+ # Returns true if the current session is shapeshifted
333
+ shapeshifter?
334
+ ```
335
+
331
336
  ## Multi-Tenancy
332
337
 
333
338
  Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants such as organizations, clients, or subdomains.
334
339
 
335
- ### Setting Up Multi-Tenancy
340
+ ### Setup
336
341
 
337
- To enable multi-tenancy, override `current_tenant` method:
342
+ To isolate authentication sessions between different tenants, override the `current_tenant` method:
338
343
 
339
344
  ```rb
340
345
  class ApplicationController < ActionController::Base
@@ -349,14 +354,16 @@ class ApplicationController < ActionController::Base
349
354
  request.subdomain
350
355
 
351
356
  # Option 2: Model-based tenancy (e.g., organization)
352
- # Company.find_by(subdomain: request.subdomain)
357
+ Company.find_by(subdomain: request.subdomain)
353
358
  end
354
359
  end
355
360
  ```
356
361
 
362
+ By default, Veri assumes a single-tenant setup where `current_tenant` returns `nil`. Tenants can be represented as either a string or an `ActiveRecord` model instance.
363
+
357
364
  ### Session Tenant Access
358
365
 
359
- Sessions expose their tenant through `tenant` method:
366
+ Sessions expose their tenant through the `tenant` method:
360
367
 
361
368
  ```rb
362
369
  # Returns the tenant (string, model instance, or nil in single-tenant applications)
@@ -376,15 +383,39 @@ user.sessions.in_tenant(tenant)
376
383
  user.sessions.in_tenant(tenant).terminate_all
377
384
  ```
378
385
 
379
- ### Migration Helpers
386
+ ### User Impersonation with Tenants
387
+
388
+ When using user impersonation in a multi-tenant setup, Veri allows cross-tenant shapeshifting while preserving the original tenant context:
389
+
390
+ ```rb
391
+ # Assume another user's identity across tenants
392
+ session.shapeshift(user, tenant: company)
393
+
394
+ # Returns the original tenant when shapeshifted
395
+ session.true_tenant
396
+ ```
397
+
398
+ All other session methods work the same way in multi-tenant applications as in single-tenant applications. However, `to_true_identity` will restore both the original user and tenant.
399
+
400
+ ### Orphaned Sessions
401
+
402
+ When a tenant object is deleted from your database, its associated sessions become orphaned.
403
+
404
+ To clean up orphaned sessions, use:
405
+
406
+ ```rb
407
+ Veri::Session.prune
408
+ ```
409
+
410
+ ### Tenant Migrations
380
411
 
381
- Handle tenant changes when models are renamed or removed. These are irreversible data migrations.
412
+ When you rename or remove models used as tenants, you need to update Veri's stored data accordingly. Use these irreversible data migrations:
382
413
 
383
414
  ```rb
384
415
  # Rename a tenant class (e.g., when you rename your Organization model to Company)
385
416
  migrate_authentication_tenant!("Organization", "Company")
386
417
 
387
- # Remove orphaned tenant data (e.g., when you delete the Organization model entirely)
418
+ # Remove tenant data (e.g., when you delete the Organization model entirely)
388
419
  delete_authentication_tenant!("Organization")
389
420
  ```
390
421
 
@@ -11,6 +11,7 @@ class AddVeriAuthentication < ActiveRecord::Migration[<%= ActiveRecord::Migratio
11
11
  t.belongs_to :authenticatable, null: false, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
12
12
  t.belongs_to :original_authenticatable, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
13
13
  t.belongs_to :tenant, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
14
+ t.belongs_to :original_tenant, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
14
15
  t.datetime :shapeshifted_at
15
16
  t.datetime :last_seen_at, null: false
16
17
  t.string :ip_address
@@ -1,53 +1,15 @@
1
1
  require "active_support/core_ext/numeric/time"
2
- require "dry-configurable"
2
+ require "singleton"
3
3
 
4
4
  module Veri
5
- module Configuration
6
- extend Dry::Configurable
5
+ class Configuration
6
+ include Singleton
7
7
 
8
- module_function
8
+ attr_reader :hashing_algorithm, :inactive_session_lifetime, :total_session_lifetime, :user_model_name
9
9
 
10
- setting :hashing_algorithm,
11
- default: :argon2,
12
- reader: true,
13
- constructor: -> (value) do
14
- Veri::Inputs::HashingAlgorithm.new(
15
- value,
16
- error: Veri::ConfigurationError,
17
- message: "Invalid hashing algorithm `#{value.inspect}`, supported algorithms are: #{Veri::Configuration::HASHERS.keys.join(", ")}"
18
- ).process
19
- end
20
- setting :inactive_session_lifetime,
21
- default: nil,
22
- reader: true,
23
- constructor: -> (value) do
24
- Veri::Inputs::Duration.new(
25
- value,
26
- optional: true,
27
- error: Veri::ConfigurationError,
28
- message: "Invalid inactive session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration or nil"
29
- ).process
30
- end
31
- setting :total_session_lifetime,
32
- default: 14.days,
33
- reader: true,
34
- constructor: -> (value) do
35
- Veri::Inputs::Duration.new(
36
- value,
37
- error: Veri::ConfigurationError,
38
- message: "Invalid total session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration"
39
- ).process
40
- end
41
- setting :user_model_name,
42
- default: "User",
43
- reader: true,
44
- constructor: -> (value) do
45
- Veri::Inputs::NonEmptyString.new(
46
- value,
47
- error: Veri::ConfigurationError,
48
- message: "Invalid user model name `#{value.inspect}`, expected an ActiveRecord model name as a string"
49
- ).process
50
- end
10
+ def initialize
11
+ reset_to_defaults!
12
+ end
51
13
 
52
14
  HASHERS = {
53
15
  argon2: Veri::Password::Argon2,
@@ -55,6 +17,47 @@ module Veri
55
17
  pbkdf2: Veri::Password::Pbkdf2,
56
18
  scrypt: Veri::Password::SCrypt
57
19
  }.freeze
20
+ private_constant :HASHERS
21
+
22
+ def hashing_algorithm=(value)
23
+ @hashing_algorithm = Veri::Inputs::HashingAlgorithm.new(
24
+ value,
25
+ error: Veri::ConfigurationError,
26
+ message: "Invalid hashing algorithm `#{value.inspect}`, supported algorithms are: #{HASHERS.keys.join(", ")}"
27
+ ).process
28
+ end
29
+
30
+ def inactive_session_lifetime=(value)
31
+ @inactive_session_lifetime = Veri::Inputs::Duration.new(
32
+ value,
33
+ optional: true,
34
+ error: Veri::ConfigurationError,
35
+ message: "Invalid inactive session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration or nil"
36
+ ).process
37
+ end
38
+
39
+ def total_session_lifetime=(value)
40
+ @total_session_lifetime = Veri::Inputs::Duration.new(
41
+ value,
42
+ error: Veri::ConfigurationError,
43
+ message: "Invalid total session lifetime `#{value.inspect}`, expected an instance of ActiveSupport::Duration"
44
+ ).process
45
+ end
46
+
47
+ def user_model_name=(value)
48
+ @user_model_name = Veri::Inputs::NonEmptyString.new(
49
+ value,
50
+ error: Veri::ConfigurationError,
51
+ message: "Invalid user model name `#{value.inspect}`, expected an ActiveRecord model name as a string"
52
+ ).process
53
+ end
54
+
55
+ def reset_to_defaults!
56
+ self.hashing_algorithm = :argon2
57
+ self.inactive_session_lifetime = nil
58
+ self.total_session_lifetime = 14.days
59
+ self.user_model_name = "User"
60
+ end
58
61
 
59
62
  def hasher
60
63
  HASHERS.fetch(hashing_algorithm) { raise Veri::Error, "Invalid hashing algorithm: #{hashing_algorithm}" }
@@ -67,5 +70,16 @@ module Veri
67
70
  message: "Invalid user model name `#{user_model_name}`, expected an ActiveRecord model name as a string"
68
71
  ).process
69
72
  end
73
+
74
+ def configure
75
+ yield self
76
+ end
77
+
78
+ class << self
79
+ delegate :hashing_algorithm, :inactive_session_lifetime, :total_session_lifetime, :user_model_name,
80
+ :hashing_algorithm=, :inactive_session_lifetime=, :total_session_lifetime=, :user_model_name=,
81
+ :hasher, :user_model, :reset_to_defaults!, :configure,
82
+ to: :instance
83
+ end
70
84
  end
71
85
  end
@@ -88,7 +88,7 @@ module Veri
88
88
  end
89
89
 
90
90
  def when_unauthenticated
91
- request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
91
+ request.format.html? ? redirect_back_or_to(root_path) : head(:unauthorized)
92
92
  end
93
93
 
94
94
  def current_tenant = nil
@@ -3,7 +3,7 @@ module Veri
3
3
  class Authenticatable < Veri::Inputs::Base
4
4
  private
5
5
 
6
- def type = -> { self.class::Instance(Veri::Configuration.user_model) }
6
+ def processor = -> { @value.is_a?(Veri::Configuration.user_model) ? @value : raise_error }
7
7
  end
8
8
  end
9
9
  end
@@ -1,10 +1,6 @@
1
- require "dry-types"
2
-
3
1
  module Veri
4
2
  module Inputs
5
3
  class Base
6
- include Dry.Types()
7
-
8
4
  def initialize(value, optional: false, error: Veri::InvalidArgumentError, message: nil)
9
5
  @value = value
10
6
  @optional = optional
@@ -13,15 +9,14 @@ module Veri
13
9
  end
14
10
 
15
11
  def process
16
- type_checker = @optional ? type.call.optional : type.call
17
- type_checker[@value]
18
- rescue Dry::Types::CoercionError
19
- raise_error
12
+ return @value if @value.nil? && @optional
13
+
14
+ processor.call
20
15
  end
21
16
 
22
17
  private
23
18
 
24
- def type
19
+ def processor
25
20
  raise NotImplementedError
26
21
  end
27
22
 
@@ -3,7 +3,7 @@ module Veri
3
3
  class Duration < Veri::Inputs::Base
4
4
  private
5
5
 
6
- def type = -> { self.class::Instance(ActiveSupport::Duration) }
6
+ def processor = -> { @value.is_a?(ActiveSupport::Duration) ? @value : raise_error }
7
7
  end
8
8
  end
9
9
  end
@@ -1,9 +1,12 @@
1
1
  module Veri
2
2
  module Inputs
3
3
  class HashingAlgorithm < Veri::Inputs::Base
4
+ HASHING_ALGORITHMS = [:argon2, :bcrypt, :pbkdf2, :scrypt].freeze
5
+ private_constant :HASHING_ALGORITHMS
6
+
4
7
  private
5
8
 
6
- def type = -> { self.class::Strict::Symbol.enum(:argon2, :bcrypt, :pbkdf2, :scrypt) }
9
+ def processor = -> { HASHING_ALGORITHMS.include?(@value) ? @value : raise_error }
7
10
  end
8
11
  end
9
12
  end
@@ -3,7 +3,13 @@ module Veri
3
3
  class Model < Veri::Inputs::Base
4
4
  private
5
5
 
6
- def type = -> { self.class::Strict::Class.constructor { _1.try(:safe_constantize) }.constrained(lt: ActiveRecord::Base) }
6
+ def processor
7
+ -> {
8
+ model = @value.try(:safe_constantize) || @value
9
+ raise_error unless model.is_a?(Class) && model < ActiveRecord::Base
10
+ model
11
+ }
12
+ end
7
13
  end
8
14
  end
9
15
  end
@@ -3,7 +3,7 @@ module Veri
3
3
  class NonEmptyString < Veri::Inputs::Base
4
4
  private
5
5
 
6
- def type = -> { self.class::Strict::String.constrained(min_size: 1) }
6
+ def processor = -> { @value.is_a?(String) && !@value.empty? ? @value : raise_error }
7
7
  end
8
8
  end
9
9
  end
@@ -1,12 +1,12 @@
1
1
  module Veri
2
2
  module Inputs
3
- class Tenant < Base
3
+ class Tenant < Veri::Inputs::Base
4
4
  def resolve
5
5
  case tenant = process
6
6
  when nil
7
7
  { tenant_type: nil, tenant_id: nil }
8
8
  when String
9
- { tenant_type: tenant.to_s, tenant_id: nil }
9
+ { tenant_type: tenant, tenant_id: nil }
10
10
  when ActiveRecord::Base
11
11
  raise_error unless tenant.persisted?
12
12
  { tenant_type: tenant.class.to_s, tenant_id: tenant.public_send(tenant.class.primary_key) }
@@ -17,15 +17,14 @@ module Veri
17
17
 
18
18
  private
19
19
 
20
- def type
20
+ def processor
21
21
  -> {
22
- self.class::Strict::String.constrained(min_size: 1) |
23
- self.class::Instance(ActiveRecord::Base) |
24
- self.class::Hash.schema(
25
- tenant_type: self.class::Strict::String | self.class::Nil,
26
- tenant_id: self.class::Strict::String | self.class::Strict::Integer | self.class::Nil
27
- ) |
28
- self.class::Nil
22
+ return @value if @value.nil?
23
+ return @value if @value.is_a?(String) && @value.present?
24
+ return @value if @value.is_a?(ActiveRecord::Base)
25
+ return @value if @value in { tenant_type: String | nil, tenant_id: String | Integer | nil }
26
+
27
+ raise_error
29
28
  }
30
29
  end
31
30
  end
@@ -7,6 +7,7 @@ module Veri
7
7
  belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
8
8
  belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
9
9
  belongs_to :tenant, polymorphic: true, optional: true
10
+ belongs_to :original_tenant, polymorphic: true, optional: true
10
11
 
11
12
  scope :in_tenant, -> (tenant) { where(**Veri::Inputs::Tenant.new(tenant).resolve) }
12
13
  scope :active, -> { where.not(id: expired.select(:id)).where.not(id: inactive.select(:id)) }
@@ -55,11 +56,23 @@ module Veri
55
56
  def identity = authenticatable
56
57
  def shapeshifted? = original_authenticatable.present?
57
58
  def true_identity = original_authenticatable || authenticatable
59
+ def true_tenant = original_tenant || tenant
60
+
61
+ def shapeshift(user, tenant: nil)
62
+ raise Veri::Error, "Cannot shapeshift from a shapeshifted session" if shapeshifted?
63
+
64
+ resolved_tenant = Veri::Inputs::Tenant.new(
65
+ tenant,
66
+ error: Veri::InvalidTenantError,
67
+ message: "Expected a string, an ActiveRecord model instance, or nil, got `#{tenant.inspect}`"
68
+ ).resolve
58
69
 
59
- def shapeshift(user)
60
70
  update!(
61
71
  shapeshifted_at: Time.current,
62
72
  original_authenticatable: authenticatable,
73
+ original_tenant_type: tenant_type,
74
+ original_tenant_id: tenant_id,
75
+ **resolved_tenant,
63
76
  authenticatable: Veri::Inputs::Authenticatable.new(
64
77
  user,
65
78
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
@@ -71,18 +84,20 @@ module Veri
71
84
  update!(
72
85
  shapeshifted_at: nil,
73
86
  authenticatable: original_authenticatable,
87
+ tenant_type: original_tenant_type,
88
+ tenant_id: original_tenant_id,
89
+ original_tenant_type: nil,
90
+ original_tenant_id: nil,
74
91
  original_authenticatable: nil
75
92
  )
76
93
  end
77
94
 
78
95
  def tenant
79
- return tenant_type if tenant_type.present? && tenant_id.blank?
80
-
81
- record = super
82
-
83
- raise ActiveRecord::RecordNotFound.new(nil, tenant_type, nil, tenant_id) if tenant_id.present? && !record
96
+ resolve_tenant(tenant_type, tenant_id) { super }
97
+ end
84
98
 
85
- record
99
+ def original_tenant
100
+ resolve_tenant(original_tenant_type, original_tenant_id) { super }
86
101
  end
87
102
 
88
103
  class << self
@@ -101,8 +116,6 @@ module Veri
101
116
  end
102
117
 
103
118
  def prune
104
- expired.or(inactive).delete_all
105
-
106
119
  orphaned_tenant_sessions = where.not(tenant_id: nil).includes(:tenant).filter_map do |session|
107
120
  !session.tenant
108
121
  rescue ActiveRecord::RecordNotFound
@@ -114,5 +127,17 @@ module Veri
114
127
 
115
128
  alias terminate_all delete_all
116
129
  end
130
+
131
+ private
132
+
133
+ def resolve_tenant(type, id)
134
+ return type if type.present? && id.blank?
135
+
136
+ record = yield
137
+
138
+ raise ActiveRecord::RecordNotFound.new(nil, type, nil, id) if id.present? && !record
139
+
140
+ record
141
+ end
117
142
  end
118
143
  end
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "1.1.0".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
data/veri.gemspec CHANGED
@@ -4,18 +4,20 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "veri"
5
5
  spec.version = Veri::VERSION
6
6
  spec.authors = ["enjaku4"]
7
- spec.email = ["enjaku4@icloud.com"]
7
+ spec.email = ["contact@brownbox.dev"]
8
8
  spec.homepage = "https://github.com/enjaku4/veri"
9
9
  spec.metadata["homepage_uri"] = spec.homepage
10
10
  spec.metadata["source_code_uri"] = spec.homepage
11
11
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
12
12
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
13
13
  spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
14
+ spec.metadata["mailing_list_uri"] = "#{spec.homepage}/discussions"
15
+ spec.metadata["funding_uri"] = "https://github.com/sponsors/enjaku4"
14
16
  spec.metadata["rubygems_mfa_required"] = "true"
15
17
  spec.summary = "Minimal cookie-based authentication library for Ruby on Rails"
16
- spec.description = "Veri provides cookie-based authentication for Ruby on Rails applications with secure password storage, granular session management, multi-tenancy support, and user impersonation feature, without imposing business logic"
18
+ spec.description = "Minimal cookie-based authentication for Rails applications with multi-tenancy support and granular session management"
17
19
  spec.license = "MIT"
18
- spec.required_ruby_version = ">= 3.2", "< 3.5"
20
+ spec.required_ruby_version = ">= 3.2", "< 4.1"
19
21
 
20
22
  spec.files = [
21
23
  "veri.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"
@@ -25,8 +27,6 @@ Gem::Specification.new do |spec|
25
27
 
26
28
  spec.add_dependency "argon2", "~> 2.0"
27
29
  spec.add_dependency "bcrypt", "~> 3.0"
28
- spec.add_dependency "dry-configurable", "~> 1.1"
29
- spec.add_dependency "dry-types", "~> 1.7"
30
30
  spec.add_dependency "rails", ">= 7.2", "< 8.2"
31
31
  spec.add_dependency "scrypt", "~> 3.0"
32
32
  spec.add_dependency "user_agent_parser", "~> 2.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: veri
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -37,34 +37,6 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.0'
40
- - !ruby/object:Gem::Dependency
41
- name: dry-configurable
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '1.1'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.1'
54
- - !ruby/object:Gem::Dependency
55
- name: dry-types
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.7'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '1.7'
68
40
  - !ruby/object:Gem::Dependency
69
41
  name: rails
70
42
  requirement: !ruby/object:Gem::Requirement
@@ -113,11 +85,10 @@ dependencies:
113
85
  - - "~>"
114
86
  - !ruby/object:Gem::Version
115
87
  version: '2.0'
116
- description: Veri provides cookie-based authentication for Ruby on Rails applications
117
- with secure password storage, granular session management, multi-tenancy support,
118
- and user impersonation feature, without imposing business logic
88
+ description: Minimal cookie-based authentication for Rails applications with multi-tenancy
89
+ support and granular session management
119
90
  email:
120
- - enjaku4@icloud.com
91
+ - contact@brownbox.dev
121
92
  executables: []
122
93
  extensions: []
123
94
  extra_rdoc_files: []
@@ -156,6 +127,8 @@ metadata:
156
127
  changelog_uri: https://github.com/enjaku4/veri/blob/main/CHANGELOG.md
157
128
  bug_tracker_uri: https://github.com/enjaku4/veri/issues
158
129
  documentation_uri: https://github.com/enjaku4/veri/blob/main/README.md
130
+ mailing_list_uri: https://github.com/enjaku4/veri/discussions
131
+ funding_uri: https://github.com/sponsors/enjaku4
159
132
  rubygems_mfa_required: 'true'
160
133
  rdoc_options: []
161
134
  require_paths:
@@ -167,14 +140,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
167
140
  version: '3.2'
168
141
  - - "<"
169
142
  - !ruby/object:Gem::Version
170
- version: '3.5'
143
+ version: '4.1'
171
144
  required_rubygems_version: !ruby/object:Gem::Requirement
172
145
  requirements:
173
146
  - - ">="
174
147
  - !ruby/object:Gem::Version
175
148
  version: '0'
176
149
  requirements: []
177
- rubygems_version: 3.7.2
150
+ rubygems_version: 4.0.0
178
151
  specification_version: 4
179
152
  summary: Minimal cookie-based authentication library for Ruby on Rails
180
153
  test_files: []