veri 2.0.2 → 2.1.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 +27 -0
- data/README.md +54 -17
- data/lib/veri/controllers/concerns/authentication.rb +14 -27
- data/lib/veri/inputs/authenticatable.rb +2 -0
- data/lib/veri/inputs/base.rb +5 -2
- data/lib/veri/inputs/non_empty_string.rb +2 -0
- data/lib/veri/inputs/tenant.rb +3 -0
- data/lib/veri/models/concerns/authenticatable.rb +22 -6
- data/lib/veri/models/session.rb +10 -13
- data/lib/veri/password/argon2.rb +4 -0
- data/lib/veri/password/bcrypt.rb +4 -0
- data/lib/veri/password/pbkdf2.rb +4 -0
- data/lib/veri/password/scrypt.rb +4 -0
- data/lib/veri/railtie.rb +10 -2
- data/lib/veri/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 703094a8e90e3fde6888629b9702406548122bed4ab199e1a02eac174c68ee07
|
|
4
|
+
data.tar.gz: 337701b97ed23c8cffd7b8ffb6d649e3e520c09bc61de6ed69e7c9d8b1905462
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e717d0cf3a4a8d9c673919548c739820ed15b42e7ab7140c9a95cd0f4740cd20a1678cf2a9dfbbcbaeb1257b60f9b8b2a28c2030d9b893fbf382a309e90b1445
|
|
7
|
+
data.tar.gz: 515e5fee62f96f4ddf4982419207565a905faa00246b5d1b483581ba86fd1063e0bbe06eaa253274c6e73d69395b1f8afd296210afd8aed3c067e264d872d7a4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## v2.1.0
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- Added `Veri::Session#switch_tenant` method to move a session to another tenant
|
|
6
|
+
- Added `password_set?` method to check whether a user has a password
|
|
7
|
+
|
|
8
|
+
### Bugs
|
|
9
|
+
|
|
10
|
+
- Fixed `log_in` using the previous tenant when `current_tenant` changes within the same request
|
|
11
|
+
- Fixed `Veri::Session.in_tenant` raising an error without a message when given an invalid tenant
|
|
12
|
+
|
|
13
|
+
## v2.0.3
|
|
14
|
+
|
|
15
|
+
### Bugs
|
|
16
|
+
|
|
17
|
+
- Fixed authentication raising `NameError` when the configured user model is not named `User`
|
|
18
|
+
- Fixed Rails commands failing when the configured user model class does not exist yet
|
|
19
|
+
- Fixed expired and inactive sessions still appearing as logged in on pages that don't require authentication
|
|
20
|
+
- Fixed locking a user not terminating their sessions until their next request
|
|
21
|
+
- Fixed password verification raising an error for users who have no password set
|
|
22
|
+
- Fixed cross-tenant shapeshifting not working
|
|
23
|
+
|
|
24
|
+
### Misc
|
|
25
|
+
|
|
26
|
+
- The hashing algorithm can now be changed without breaking existing passwords
|
|
27
|
+
|
|
1
28
|
## v2.0.2
|
|
2
29
|
|
|
3
30
|
### Misc
|
data/README.md
CHANGED
|
@@ -81,6 +81,8 @@ Veri.configure do |config|
|
|
|
81
81
|
end
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
The hashing algorithm can be changed at any time: existing passwords are verified with the algorithm they were hashed with and are automatically re-hashed with the configured algorithm on the next successful login.
|
|
85
|
+
|
|
84
86
|
## Password Management
|
|
85
87
|
|
|
86
88
|
Your user model is automatically extended with password management methods:
|
|
@@ -91,6 +93,9 @@ user.update_password("password")
|
|
|
91
93
|
|
|
92
94
|
# Verify a password
|
|
93
95
|
user.verify_password("password")
|
|
96
|
+
|
|
97
|
+
# Check if a password is set
|
|
98
|
+
user.password_set?
|
|
94
99
|
```
|
|
95
100
|
Changing a password does not automatically terminate existing sessions. If you want to invalidate the user's sessions after a password change, do so explicitly:
|
|
96
101
|
|
|
@@ -396,6 +401,40 @@ user.sessions.in_tenant(tenant)
|
|
|
396
401
|
user.sessions.in_tenant(tenant).terminate_all
|
|
397
402
|
```
|
|
398
403
|
|
|
404
|
+
### Switching Tenants
|
|
405
|
+
|
|
406
|
+
A session belongs to one tenant, and a browser holds one session per host. A user is therefore logged in to one tenant at a time, unless your tenants live on separate subdomains, where each subdomain keeps its own session.
|
|
407
|
+
|
|
408
|
+
To move a user to another tenant, call `switch_tenant` on their session and make `current_tenant` return the new tenant from then on:
|
|
409
|
+
|
|
410
|
+
```rb
|
|
411
|
+
class ApplicationController < ActionController::Base
|
|
412
|
+
include Veri::Authentication
|
|
413
|
+
|
|
414
|
+
with_authentication
|
|
415
|
+
|
|
416
|
+
private
|
|
417
|
+
|
|
418
|
+
def current_tenant
|
|
419
|
+
# This example keeps the tenant in a cookie; your app can keep it anywhere
|
|
420
|
+
Company.find_by(id: cookies.signed[:company_id])
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
class CompaniesController < ApplicationController
|
|
425
|
+
def switch
|
|
426
|
+
company = current_user.companies.find(params[:id])
|
|
427
|
+
|
|
428
|
+
current_session.switch_tenant(company)
|
|
429
|
+
cookies.signed[:company_id] = company.id
|
|
430
|
+
|
|
431
|
+
redirect_to root_path, notice: "Switched to #{company.name}"
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
The user stays logged in and the session keeps its expiration.
|
|
437
|
+
|
|
399
438
|
### User Impersonation with Tenants
|
|
400
439
|
|
|
401
440
|
When using user impersonation in a multi-tenant setup, Veri allows cross-tenant shapeshifting while preserving the original tenant context:
|
|
@@ -408,24 +447,20 @@ session.shapeshift(user, tenant: company)
|
|
|
408
447
|
session.true_tenant
|
|
409
448
|
```
|
|
410
449
|
|
|
450
|
+
The `tenant:` argument always becomes the session's tenant: omitting it means no tenant. In a multi-tenant application, always pass it explicitly.
|
|
451
|
+
|
|
411
452
|
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.
|
|
412
453
|
|
|
413
454
|
### Orphaned Sessions
|
|
414
455
|
|
|
415
456
|
When a tenant object is deleted from your database, its associated sessions become orphaned.
|
|
416
457
|
|
|
417
|
-
|
|
458
|
+
Ideally, the application should be designed so that this doesn't happen: whenever a tenant object is deleted, its sessions are terminated as part of the same operation. However, if that wasn't done, you can clean up the orphaned sessions as a fallback:
|
|
418
459
|
|
|
419
460
|
```rb
|
|
420
461
|
Veri::Session.prune
|
|
421
462
|
```
|
|
422
463
|
|
|
423
|
-
Or, for a specific user:
|
|
424
|
-
|
|
425
|
-
```rb
|
|
426
|
-
user.sessions.prune
|
|
427
|
-
```
|
|
428
|
-
|
|
429
464
|
### Tenant Migrations
|
|
430
465
|
|
|
431
466
|
When you rename or remove models used as tenants, you need to update Veri's stored data accordingly. Use these irreversible data migrations:
|
|
@@ -502,24 +537,26 @@ end
|
|
|
502
537
|
## Getting Help and Contributing
|
|
503
538
|
|
|
504
539
|
### Getting Help
|
|
540
|
+
|
|
505
541
|
Have a question or need assistance? Open a discussion in the [discussions section](https://github.com/enjaku4/veri/discussions) for:
|
|
542
|
+
|
|
506
543
|
- Usage questions
|
|
507
544
|
- Implementation guidance
|
|
508
|
-
-
|
|
545
|
+
- Open-ended ideas or suggestions
|
|
546
|
+
|
|
547
|
+
### Issues
|
|
548
|
+
|
|
549
|
+
[Issues](https://github.com/enjaku4/veri/issues) track bugs, planned features, and other work. When reporting a bug, please include:
|
|
509
550
|
|
|
510
|
-
### Reporting Issues
|
|
511
|
-
Found a bug? Please [create an issue](https://github.com/enjaku4/veri/issues) with:
|
|
512
551
|
- A clear description of the problem
|
|
513
552
|
- Steps to reproduce the issue
|
|
514
|
-
- Your environment details (Rails
|
|
553
|
+
- Your environment details (Rails and Ruby versions, OS, etc.)
|
|
515
554
|
|
|
516
555
|
### Contributing Code
|
|
517
|
-
Ready to contribute? You can:
|
|
518
|
-
- Fix bugs by submitting pull requests
|
|
519
|
-
- Improve documentation
|
|
520
|
-
- Add new features (please discuss first in the [discussions section](https://github.com/enjaku4/veri/discussions))
|
|
521
556
|
|
|
522
|
-
|
|
557
|
+
Any open issue is free to pick up or discuss. Check the [project board](https://github.com/users/enjaku4/projects/13) or [issues tab](https://github.com/enjaku4/veri/issues).
|
|
558
|
+
|
|
559
|
+
Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/veri/blob/main/CONTRIBUTING.md).
|
|
523
560
|
|
|
524
561
|
## License
|
|
525
562
|
|
|
@@ -531,6 +568,6 @@ Everyone interacting in the Veri project is expected to follow the [code of cond
|
|
|
531
568
|
|
|
532
569
|
## Old Versions
|
|
533
570
|
|
|
534
|
-
Only the latest major version is
|
|
571
|
+
Only the latest major version is actively maintained. READMEs for older versions are available here for reference:
|
|
535
572
|
|
|
536
573
|
[v1.x.x](https://github.com/enjaku4/veri/blob/9c188e16a703141b7cd89dd31d5cd49a557f143d/README.md)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require "zlib"
|
|
2
|
-
|
|
3
1
|
module Veri
|
|
4
2
|
module Authentication
|
|
5
3
|
extend ActiveSupport::Concern
|
|
@@ -29,28 +27,27 @@ module Veri
|
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
def current_session
|
|
32
|
-
token = cookies.encrypted["
|
|
30
|
+
token = cookies.encrypted["veri_token"]
|
|
33
31
|
|
|
34
|
-
@current_session ||= Session.
|
|
32
|
+
@current_session ||= Session.find_active(token, resolved_tenant)
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
def log_in(authenticatable)
|
|
38
|
-
processed_authenticatable = Veri::Inputs::Authenticatable.new(
|
|
39
|
-
authenticatable,
|
|
40
|
-
message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{authenticatable.inspect}`"
|
|
41
|
-
).process
|
|
36
|
+
processed_authenticatable = Veri::Inputs::Authenticatable.new(authenticatable).process
|
|
42
37
|
|
|
43
38
|
return false if processed_authenticatable.locked?
|
|
44
39
|
|
|
45
40
|
token = Veri::Session.establish(processed_authenticatable, request, resolved_tenant)
|
|
46
41
|
|
|
47
|
-
cookies.encrypted.permanent["
|
|
42
|
+
cookies.encrypted.permanent["veri_token"] = { value: token, httponly: true, secure: request.ssl? }
|
|
43
|
+
reset_memoization
|
|
48
44
|
true
|
|
49
45
|
end
|
|
50
46
|
|
|
51
47
|
def log_out
|
|
52
48
|
current_session&.terminate
|
|
53
|
-
cookies.delete("
|
|
49
|
+
cookies.delete("veri_token")
|
|
50
|
+
reset_memoization
|
|
54
51
|
end
|
|
55
52
|
|
|
56
53
|
def logged_in?
|
|
@@ -58,7 +55,7 @@ module Veri
|
|
|
58
55
|
end
|
|
59
56
|
|
|
60
57
|
def return_path
|
|
61
|
-
cookies.signed["
|
|
58
|
+
cookies.signed["veri_return_path"]
|
|
62
59
|
end
|
|
63
60
|
|
|
64
61
|
def shapeshifter?
|
|
@@ -68,20 +65,14 @@ module Veri
|
|
|
68
65
|
private
|
|
69
66
|
|
|
70
67
|
def with_authentication
|
|
71
|
-
if logged_in? &&
|
|
72
|
-
|
|
73
|
-
log_out
|
|
74
|
-
when_unauthenticated
|
|
75
|
-
else
|
|
76
|
-
current_session.update_info(request)
|
|
77
|
-
end
|
|
78
|
-
|
|
68
|
+
if logged_in? && !current_user.locked?
|
|
69
|
+
current_session.update_info(request)
|
|
79
70
|
return
|
|
80
71
|
end
|
|
81
72
|
|
|
82
73
|
log_out
|
|
83
74
|
|
|
84
|
-
cookies.signed["
|
|
75
|
+
cookies.signed["veri_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
|
|
85
76
|
|
|
86
77
|
when_unauthenticated
|
|
87
78
|
end
|
|
@@ -93,15 +84,11 @@ module Veri
|
|
|
93
84
|
def current_tenant = nil
|
|
94
85
|
|
|
95
86
|
def resolved_tenant
|
|
96
|
-
|
|
97
|
-
current_tenant,
|
|
98
|
-
error: Veri::InvalidTenantError,
|
|
99
|
-
message: "Expected a string, an ActiveRecord model instance, or nil, got `#{current_tenant.inspect}`"
|
|
100
|
-
).resolve
|
|
87
|
+
Veri::Inputs::Tenant.new(current_tenant).resolve
|
|
101
88
|
end
|
|
102
89
|
|
|
103
|
-
def
|
|
104
|
-
@
|
|
90
|
+
def reset_memoization
|
|
91
|
+
@current_user = @current_session = nil
|
|
105
92
|
end
|
|
106
93
|
end
|
|
107
94
|
end
|
|
@@ -3,6 +3,8 @@ module Veri
|
|
|
3
3
|
class Authenticatable < Veri::Inputs::Base
|
|
4
4
|
private
|
|
5
5
|
|
|
6
|
+
def default_message = "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{@value.inspect}`"
|
|
7
|
+
|
|
6
8
|
def processor = -> { @value.is_a?(Veri::Configuration.user_model) ? @value : raise_error }
|
|
7
9
|
end
|
|
8
10
|
end
|
data/lib/veri/inputs/base.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Veri
|
|
2
2
|
module Inputs
|
|
3
3
|
class Base
|
|
4
|
-
def initialize(value, optional: false, error:
|
|
4
|
+
def initialize(value, optional: false, error: nil, message: nil)
|
|
5
5
|
@value = value
|
|
6
6
|
@optional = optional
|
|
7
7
|
@error = error
|
|
@@ -20,8 +20,11 @@ module Veri
|
|
|
20
20
|
raise NotImplementedError
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def default_error = Veri::InvalidArgumentError
|
|
24
|
+
def default_message = nil
|
|
25
|
+
|
|
23
26
|
def raise_error
|
|
24
|
-
raise @error, @message
|
|
27
|
+
raise @error || default_error, @message || default_message
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
end
|
data/lib/veri/inputs/tenant.rb
CHANGED
|
@@ -16,21 +16,37 @@ module Veri
|
|
|
16
16
|
def update_password(password)
|
|
17
17
|
update!(
|
|
18
18
|
hashed_password: hasher.create(
|
|
19
|
-
Veri::Inputs::NonEmptyString.new(password
|
|
19
|
+
Veri::Inputs::NonEmptyString.new(password).process
|
|
20
20
|
),
|
|
21
21
|
password_updated_at: Time.current
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def password_set?
|
|
26
|
+
hashed_password.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def verify_password(password)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
processed_password = Veri::Inputs::NonEmptyString.new(password).process
|
|
31
|
+
|
|
32
|
+
return false unless password_set?
|
|
33
|
+
|
|
34
|
+
stored_hasher = Veri::Configuration::HASHERS.values.find { _1.match?(hashed_password) }
|
|
35
|
+
|
|
36
|
+
raise Veri::Error, "Unrecognized password hash format" unless stored_hasher
|
|
37
|
+
|
|
38
|
+
return false unless stored_hasher.verify(processed_password, hashed_password)
|
|
39
|
+
|
|
40
|
+
update_column(:hashed_password, hasher.create(processed_password)) unless stored_hasher == hasher
|
|
41
|
+
|
|
42
|
+
true
|
|
30
43
|
end
|
|
31
44
|
|
|
32
45
|
def lock!
|
|
33
|
-
|
|
46
|
+
transaction do
|
|
47
|
+
update!(locked: true, locked_at: Time.current)
|
|
48
|
+
sessions.terminate_all
|
|
49
|
+
end
|
|
34
50
|
end
|
|
35
51
|
|
|
36
52
|
def unlock!
|
data/lib/veri/models/session.rb
CHANGED
|
@@ -5,8 +5,6 @@ module Veri
|
|
|
5
5
|
class Session < ActiveRecord::Base
|
|
6
6
|
self.table_name = "veri_sessions"
|
|
7
7
|
|
|
8
|
-
belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
|
|
9
|
-
belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
|
|
10
8
|
belongs_to :tenant, polymorphic: true, optional: true
|
|
11
9
|
belongs_to :original_tenant, polymorphic: true, optional: true
|
|
12
10
|
|
|
@@ -62,11 +60,7 @@ module Veri
|
|
|
62
60
|
def shapeshift(user, tenant: nil)
|
|
63
61
|
raise Veri::Error, "Cannot shapeshift from a shapeshifted session" if shapeshifted?
|
|
64
62
|
|
|
65
|
-
resolved_tenant = Veri::Inputs::Tenant.new(
|
|
66
|
-
tenant,
|
|
67
|
-
error: Veri::InvalidTenantError,
|
|
68
|
-
message: "Expected a string, an ActiveRecord model instance, or nil, got `#{tenant.inspect}`"
|
|
69
|
-
).resolve
|
|
63
|
+
resolved_tenant = Veri::Inputs::Tenant.new(tenant).resolve
|
|
70
64
|
|
|
71
65
|
update!(
|
|
72
66
|
shapeshifted_at: Time.current,
|
|
@@ -74,10 +68,7 @@ module Veri
|
|
|
74
68
|
original_tenant_type: tenant_type,
|
|
75
69
|
original_tenant_id: tenant_id,
|
|
76
70
|
**resolved_tenant,
|
|
77
|
-
authenticatable: Veri::Inputs::Authenticatable.new(
|
|
78
|
-
user,
|
|
79
|
-
message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
|
|
80
|
-
).process
|
|
71
|
+
authenticatable: Veri::Inputs::Authenticatable.new(user).process
|
|
81
72
|
)
|
|
82
73
|
end
|
|
83
74
|
|
|
@@ -93,6 +84,10 @@ module Veri
|
|
|
93
84
|
)
|
|
94
85
|
end
|
|
95
86
|
|
|
87
|
+
def switch_tenant(tenant)
|
|
88
|
+
update!(**Veri::Inputs::Tenant.new(tenant).resolve)
|
|
89
|
+
end
|
|
90
|
+
|
|
96
91
|
def tenant
|
|
97
92
|
resolve_tenant(tenant_type, tenant_id) { super }
|
|
98
93
|
end
|
|
@@ -128,10 +123,12 @@ module Veri
|
|
|
128
123
|
|
|
129
124
|
alias terminate_all delete_all
|
|
130
125
|
|
|
131
|
-
def
|
|
126
|
+
def find_active(token, resolved_tenant)
|
|
132
127
|
return nil if token.blank?
|
|
133
128
|
|
|
134
|
-
find_by(hashed_token: digest_token(token), **resolved_tenant)
|
|
129
|
+
session = find_by(hashed_token: digest_token(token), **resolved_tenant)
|
|
130
|
+
|
|
131
|
+
session&.active? ? session : nil
|
|
135
132
|
end
|
|
136
133
|
|
|
137
134
|
private
|
data/lib/veri/password/argon2.rb
CHANGED
data/lib/veri/password/bcrypt.rb
CHANGED
data/lib/veri/password/pbkdf2.rb
CHANGED
data/lib/veri/password/scrypt.rb
CHANGED
data/lib/veri/railtie.rb
CHANGED
|
@@ -22,8 +22,16 @@ module Veri
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
begin
|
|
26
|
+
user_model = Veri::Configuration.user_model
|
|
27
|
+
user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
|
|
28
|
+
rescue Veri::ConfigurationError
|
|
29
|
+
raise if Veri::Railtie.server_running?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Veri::Session.belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
|
|
33
|
+
Veri::Session.belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name,
|
|
34
|
+
optional: true
|
|
27
35
|
end
|
|
28
36
|
end
|
|
29
37
|
|
data/lib/veri/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: veri
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- enjaku4
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: argon2
|
|
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
147
147
|
- !ruby/object:Gem::Version
|
|
148
148
|
version: '0'
|
|
149
149
|
requirements: []
|
|
150
|
-
rubygems_version: 4.0.
|
|
150
|
+
rubygems_version: 4.0.16
|
|
151
151
|
specification_version: 4
|
|
152
152
|
summary: Minimal cookie-based authentication library for Ruby on Rails
|
|
153
153
|
test_files: []
|