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 +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +115 -77
- 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 +35 -9
- data/lib/veri/version.rb +1 -1
- data/veri.gemspec +6 -6
- metadata +10 -37
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,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
|
[](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
9
|
|
|
10
|
-
|
|
10
|
+
Veri supports multi-tenancy, granular session management, multiple password hashing algorithms, and provides a user impersonation feature for administration purposes.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
```
|
|
45
|
+
```shell
|
|
49
46
|
bundle install
|
|
50
47
|
```
|
|
51
48
|
|
|
52
|
-
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):
|
|
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
|
-
###
|
|
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
|
-
###
|
|
176
|
+
### When Unauthenticated
|
|
178
177
|
|
|
179
|
-
Veri
|
|
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
|
|
308
|
-
|
|
261
|
+
# Clean up inactive sessions for a specific user
|
|
262
|
+
user.sessions.inactive.terminate_all
|
|
309
263
|
|
|
310
|
-
# Clean up expired
|
|
311
|
-
|
|
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
|
|
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
|
|
338
|
+
Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants such as organizations, clients, or subdomains.
|
|
340
339
|
|
|
341
|
-
###
|
|
340
|
+
### Setup
|
|
342
341
|
|
|
343
|
-
To
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
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,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
|
-
|
|
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
|
-
|
|
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
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,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 "
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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: []
|