veri 1.0.1 → 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: 17672738e498e3328b8b4cf724e3b30e4ee3fb2f7c1d768344cd0c9d29ced1df
4
- data.tar.gz: bb8031eb131fce46b349e6881d8b20a18d43c19bf4aba7b41ae1bc6474e5c3cd
3
+ metadata.gz: 6bd0d2ab55db163c3fe4c23ffde6b23cd161d97c28c3fba95be4f7f88afbd2ea
4
+ data.tar.gz: b1afab87840a02696726deb462bff8e53c881e27ac78adf2787ba97cc59ae7ab
5
5
  SHA512:
6
- metadata.gz: a6000202fed29f8e3e303d831727c7199cc073dcec3c592e412b3234b3a55c06b5761f4781f7325d7113a44af2c2f4786fff526b7d9bbd30eaebf599e62987e0
7
- data.tar.gz: eaa49fd164b25daefeb97c505d73b11fb148f433f940594a6e6a349eae8a341f3ad8a716eccd5e47204968685602c895185ee255144cf39680eab610d66e83c1
6
+ metadata.gz: 99b2f7cc063ebffc0fd94b96fcade3ea4d9e911f21a854146b1b09500319a693dc12d44e4bcb8d64a1382d10d33c8bae4df5321bb22fcc074f0afc9c9ca0f6bd
7
+ data.tar.gz: 2d4cc974fb4dacee177d526099d8524cd24b1b8733115a277b1b172fb84bf3e32b42a56558b57ab4552139f6c5364351708304d806cd6eb4eca22aa5497339c5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
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
+
22
+ ## v1.1.0
23
+
24
+ ### Features
25
+
26
+ - Added `Veri::Session.in_tenant` method to fetch sessions for a specific tenant
27
+
28
+ ### Misc
29
+
30
+ - Added support for Rails 8.1
31
+
1
32
  ## v1.0.1
2
33
 
3
34
  ### Bugs
data/README.md CHANGED
@@ -5,17 +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, Veri doesn't impose business logic or generate controllers, views, and mailers for you. Instead, it provides essential authentication building blocks giving you complete control over your authentication flow while handling the complex underlying mechanics of secure password storage and session management.
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
9
 
10
- **Key Features:**
10
+ Veri supports multi-tenancy, granular session management, multiple password hashing algorithms, and provides a user impersonation feature for administration purposes.
11
11
 
12
- - Cookie-based authentication with database-stored sessions
13
- - Multiple password hashing algorithms (argon2, bcrypt, pbkdf2, scrypt)
14
- - Multi-tenancy support
15
- - Granular session management and control
16
- - User impersonation feature
17
- - Account lockout functionality
18
- - Return path handling
12
+ **Example of Usage:**
13
+
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.
19
15
 
20
16
  ## Table of Contents
21
17
 
@@ -26,6 +22,7 @@ Veri is a cookie-based authentication library for Ruby on Rails. Unlike other so
26
22
  - [Controller Integration](#controller-integration)
27
23
  - [Authentication Sessions](#authentication-sessions)
28
24
  - [Account Lockout](#account-lockout)
25
+ - [User Impersonation](#user-impersonation)
29
26
  - [Multi-Tenancy](#multi-tenancy)
30
27
  - [View Helpers](#view-helpers)
31
28
  - [Testing](#testing)
@@ -45,11 +42,11 @@ gem "veri"
45
42
 
46
43
  Install the gem:
47
44
 
48
- ```bash
45
+ ```shell
49
46
  bundle install
50
47
  ```
51
48
 
52
- 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):
53
50
 
54
51
  ```shell
55
52
  # For standard integer IDs
@@ -93,9 +90,9 @@ user.verify_password("password")
93
90
 
94
91
  ## Controller Integration
95
92
 
96
- ### Basic Setup
93
+ ### Setup
97
94
 
98
- Include the authentication module and configure protection:
95
+ Include the authentication module in your controllers and configure protection:
99
96
 
100
97
  ```rb
101
98
  class ApplicationController < ActionController::Base
@@ -109,6 +106,8 @@ class PicturesController < ApplicationController
109
106
  end
110
107
  ```
111
108
 
109
+ Both `with_authentication` and `skip_authentication` work exactly the same as Rails' `before_action` and `skip_before_action` methods.
110
+
112
111
  ### Authentication Methods
113
112
 
114
113
  This is a simplified example of how to use Veri's authentication methods:
@@ -174,54 +173,9 @@ return_path
174
173
  current_session
175
174
  ```
176
175
 
177
- ### User Impersonation (Shapeshifting)
176
+ ### When Unauthenticated
178
177
 
179
- Veri provides user impersonation functionality that allows administrators to temporarily assume another user's identity:
180
-
181
- ```rb
182
- module Admin
183
- class ImpersonationController < ApplicationController
184
- def create
185
- user = User.find(params[:user_id])
186
- current_session.shapeshift(user)
187
- redirect_to root_path, notice: "Now viewing as #{user.name}"
188
- end
189
-
190
- def destroy
191
- original_user = current_session.true_identity
192
- current_session.to_true_identity
193
- redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
194
- end
195
- end
196
- end
197
- ```
198
-
199
- Available session methods:
200
-
201
- ```rb
202
- # Assume another user's identity (maintains original identity)
203
- session.shapeshift(user)
204
-
205
- # Return to original identity
206
- session.to_true_identity
207
-
208
- # Returns true if currently shapeshifted
209
- session.shapeshifted?
210
-
211
- # Returns original user when shapeshifted, otherwise current user
212
- session.true_identity
213
- ```
214
-
215
- Controller helper:
216
-
217
- ```rb
218
- # Returns true if the current session is shapeshifted
219
- shapeshifter?
220
- ```
221
-
222
- ### When unauthenticated
223
-
224
- Override this private method to customize unauthenticated user behavior:
178
+ By default, when unauthenticated, Veri redirects back (HTML) or returns 401 (other formats). Override this private method to customize behavior for unauthenticated users:
225
179
 
226
180
  ```rb
227
181
  class ApplicationController < ActionController::Base
@@ -229,17 +183,16 @@ class ApplicationController < ActionController::Base
229
183
 
230
184
  with_authentication
231
185
 
232
- # ...
233
-
234
186
  private
235
187
 
236
188
  def when_unauthenticated
237
- # By default, redirects back (HTML) or returns 401 (other formats)
238
189
  redirect_to login_path
239
190
  end
240
191
  end
241
192
  ```
242
193
 
194
+ The `when_unauthenticated` method can be overridden in any controller to provide controller-specific handling.
195
+
243
196
  ## Authentication Sessions
244
197
 
245
198
  Veri stores authentication sessions in the database, providing session management capabilities:
@@ -257,9 +210,10 @@ current_session
257
210
  ### Session Information
258
211
 
259
212
  ```rb
213
+ # Get the authenticated user
260
214
  session.identity
261
- # => authenticated user
262
215
 
216
+ # Get session details
263
217
  session.info
264
218
  # => {
265
219
  # device: "Desktop",
@@ -304,11 +258,11 @@ Veri::Session.terminate_all
304
258
  # Terminate all sessions for a specific user
305
259
  user.sessions.terminate_all
306
260
 
307
- # Clean up expired/inactive sessions, and sessions with deleted tenant
308
- Veri::Session.prune
261
+ # Clean up inactive sessions for a specific user
262
+ user.sessions.inactive.terminate_all
309
263
 
310
- # Clean up expired/inactive sessions for a specific user
311
- user.sessions.prune
264
+ # Clean up expired sessions globally
265
+ Veri::Session.expired.terminate_all
312
266
  ```
313
267
 
314
268
  ## Account Lockout
@@ -332,15 +286,60 @@ User.locked
332
286
  User.unlocked
333
287
  ```
334
288
 
335
- When an account is locked, the user cannot log in. If they're already logged in, their sessions are terminated and they're treated as unauthenticated.
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.
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
+ ```
336
335
 
337
336
  ## Multi-Tenancy
338
337
 
339
- Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants like organizations, clients, or subdomains.
338
+ Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants such as organizations, clients, or subdomains.
340
339
 
341
- ### Setting Up Multi-Tenancy
340
+ ### Setup
342
341
 
343
- To enable multi-tenancy, override `current_tenant` method:
342
+ To isolate authentication sessions between different tenants, override the `current_tenant` method:
344
343
 
345
344
  ```rb
346
345
  class ApplicationController < ActionController::Base
@@ -355,29 +354,68 @@ class ApplicationController < ActionController::Base
355
354
  request.subdomain
356
355
 
357
356
  # Option 2: Model-based tenancy (e.g., organization)
358
- # Company.find_by(subdomain: request.subdomain)
357
+ Company.find_by(subdomain: request.subdomain)
359
358
  end
360
359
  end
361
360
  ```
362
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
+
363
364
  ### Session Tenant Access
364
365
 
365
- Sessions expose their tenant through `tenant` method:
366
+ Sessions expose their tenant through the `tenant` method:
366
367
 
367
368
  ```rb
368
369
  # Returns the tenant (string, model instance, or nil in single-tenant applications)
369
370
  session.tenant
370
371
  ```
371
372
 
372
- ### Migration Helpers
373
+ To manage sessions for a specific tenant:
374
+
375
+ ```rb
376
+ # Fetch all sessions for a given tenant
377
+ Veri::Session.in_tenant(tenant)
378
+
379
+ # Fetch sessions for a specific user within a tenant
380
+ user.sessions.in_tenant(tenant)
381
+
382
+ # Terminate all sessions for a specific user within a tenant
383
+ user.sessions.in_tenant(tenant).terminate_all
384
+ ```
385
+
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
373
411
 
374
- 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:
375
413
 
376
414
  ```rb
377
415
  # Rename a tenant class (e.g., when you rename your Organization model to Company)
378
416
  migrate_authentication_tenant!("Organization", "Company")
379
417
 
380
- # 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)
381
419
  delete_authentication_tenant!("Organization")
382
420
  ```
383
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,7 +7,9 @@ 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
 
12
+ scope :in_tenant, -> (tenant) { where(**Veri::Inputs::Tenant.new(tenant).resolve) }
11
13
  scope :active, -> { where.not(id: expired.select(:id)).where.not(id: inactive.select(:id)) }
12
14
  scope :expired, -> { where(expires_at: ...Time.current) }
13
15
  scope :inactive, -> do
@@ -54,11 +56,23 @@ module Veri
54
56
  def identity = authenticatable
55
57
  def shapeshifted? = original_authenticatable.present?
56
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
57
69
 
58
- def shapeshift(user)
59
70
  update!(
60
71
  shapeshifted_at: Time.current,
61
72
  original_authenticatable: authenticatable,
73
+ original_tenant_type: tenant_type,
74
+ original_tenant_id: tenant_id,
75
+ **resolved_tenant,
62
76
  authenticatable: Veri::Inputs::Authenticatable.new(
63
77
  user,
64
78
  message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
@@ -70,18 +84,20 @@ module Veri
70
84
  update!(
71
85
  shapeshifted_at: nil,
72
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,
73
91
  original_authenticatable: nil
74
92
  )
75
93
  end
76
94
 
77
95
  def tenant
78
- return tenant_type if tenant_type.present? && tenant_id.blank?
79
-
80
- record = super
81
-
82
- 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
83
98
 
84
- record
99
+ def original_tenant
100
+ resolve_tenant(original_tenant_type, original_tenant_id) { super }
85
101
  end
86
102
 
87
103
  class << self
@@ -100,8 +116,6 @@ module Veri
100
116
  end
101
117
 
102
118
  def prune
103
- expired.or(inactive).delete_all
104
-
105
119
  orphaned_tenant_sessions = where.not(tenant_id: nil).includes(:tenant).filter_map do |session|
106
120
  !session.tenant
107
121
  rescue ActiveRecord::RecordNotFound
@@ -113,5 +127,17 @@ module Veri
113
127
 
114
128
  alias terminate_all delete_all
115
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
116
142
  end
117
143
  end
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "1.0.1".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,9 +27,7 @@ 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
- spec.add_dependency "rails", ">= 7.2", "< 8.1"
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"
33
33
  end
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.0.1
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
@@ -74,7 +46,7 @@ dependencies:
74
46
  version: '7.2'
75
47
  - - "<"
76
48
  - !ruby/object:Gem::Version
77
- version: '8.1'
49
+ version: '8.2'
78
50
  type: :runtime
79
51
  prerelease: false
80
52
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +56,7 @@ dependencies:
84
56
  version: '7.2'
85
57
  - - "<"
86
58
  - !ruby/object:Gem::Version
87
- version: '8.1'
59
+ version: '8.2'
88
60
  - !ruby/object:Gem::Dependency
89
61
  name: scrypt
90
62
  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: []