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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +98 -67
- data/lib/generators/veri/templates/add_veri_authentication.rb.erb +1 -0
- data/lib/veri/configuration.rb +59 -45
- data/lib/veri/controllers/concerns/authentication.rb +1 -1
- data/lib/veri/inputs/authenticatable.rb +1 -1
- data/lib/veri/inputs/base.rb +4 -9
- data/lib/veri/inputs/duration.rb +1 -1
- data/lib/veri/inputs/hashing_algorithm.rb +4 -1
- data/lib/veri/inputs/model.rb +7 -1
- data/lib/veri/inputs/non_empty_string.rb +1 -1
- data/lib/veri/inputs/tenant.rb +9 -10
- data/lib/veri/models/session.rb +34 -9
- data/lib/veri/version.rb +1 -1
- data/veri.gemspec +5 -5
- metadata +8 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bd0d2ab55db163c3fe4c23ffde6b23cd161d97c28c3fba95be4f7f88afbd2ea
|
|
4
|
+
data.tar.gz: b1afab87840a02696726deb462bff8e53c881e27ac78adf2787ba97cc59ae7ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://github.com/enjaku4/veri/actions/workflows/ci.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Veri is a cookie-based authentication library for Ruby on Rails.
|
|
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
|
|
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
|
-
```
|
|
45
|
+
```shell
|
|
43
46
|
bundle install
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
Generate the migration for your user model (replace `users` with your
|
|
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
|
-
###
|
|
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
|
|
302
|
-
|
|
261
|
+
# Clean up inactive sessions for a specific user
|
|
262
|
+
user.sessions.inactive.terminate_all
|
|
303
263
|
|
|
304
|
-
# Clean up expired
|
|
305
|
-
|
|
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
|
-
###
|
|
340
|
+
### Setup
|
|
336
341
|
|
|
337
|
-
To
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/veri/configuration.rb
CHANGED
|
@@ -1,53 +1,15 @@
|
|
|
1
1
|
require "active_support/core_ext/numeric/time"
|
|
2
|
-
require "
|
|
2
|
+
require "singleton"
|
|
3
3
|
|
|
4
4
|
module Veri
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
class Configuration
|
|
6
|
+
include Singleton
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
attr_reader :hashing_algorithm, :inactive_session_lifetime, :total_session_lifetime, :user_model_name
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
data/lib/veri/inputs/base.rb
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
19
|
+
def processor
|
|
25
20
|
raise NotImplementedError
|
|
26
21
|
end
|
|
27
22
|
|
data/lib/veri/inputs/duration.rb
CHANGED
|
@@ -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
|
|
9
|
+
def processor = -> { HASHING_ALGORITHMS.include?(@value) ? @value : raise_error }
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
data/lib/veri/inputs/model.rb
CHANGED
|
@@ -3,7 +3,13 @@ module Veri
|
|
|
3
3
|
class Model < Veri::Inputs::Base
|
|
4
4
|
private
|
|
5
5
|
|
|
6
|
-
def
|
|
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
|
data/lib/veri/inputs/tenant.rb
CHANGED
|
@@ -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
|
|
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
|
|
20
|
+
def processor
|
|
21
21
|
-> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
data/lib/veri/models/session.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
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 = ["
|
|
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 = "
|
|
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", "<
|
|
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:
|
|
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:
|
|
117
|
-
|
|
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
|
-
-
|
|
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: '
|
|
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:
|
|
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: []
|