verifica 1.0.0 → 1.0.2
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 +12 -0
- data/README.md +44 -34
- data/lib/verifica/acl.rb +1 -2
- data/lib/verifica/authorizer.rb +32 -15
- data/lib/verifica/sid.rb +61 -0
- data/lib/verifica/version.rb +1 -1
- data/lib/verifica.rb +20 -14
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f38a13446f6e2b3eb7e118a1d97d5369aca85116a3bde44d7dd1f07bfe013f7a
|
4
|
+
data.tar.gz: ae82a49612de5e13ed32992a13c34a3af57e50b18772c0651194459b5a187010
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcaee3d318dbada1fe7f4c135bbd38219b8031126e1e0d29b5c125a15f1867e8305fb4ec5ca91a2b4eddc4040ad49d24af9e79fb6a36ddab115f2d50db70d5fe
|
7
|
+
data.tar.gz: f0a4392ff578a8444b5f922ae7f3844bd14126ceb5570bf2e9ded02463a3ff63ec69d68b248e5b4241dfa10db4ab537b6fd35b2073e1d4d070f76d74f2c6e896
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [1.0.2] - 2023-10-25
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
|
14
|
+
- Make `Authorizer#authorization_result` public for even more usage flexibility
|
15
|
+
|
16
|
+
## [1.0.1] - 2023-04-15
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- `Sid#country_sid` and `Sid#group_sid` helper methods
|
21
|
+
|
10
22
|
## [1.0.0] - 2023-01-19
|
11
23
|
|
12
24
|
Initial public release
|
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
+
[](https://badge.fury.io/rb/verifica)
|
1
2
|
[](https://github.com/maximgurin/verifica/actions/workflows/ci.yml)
|
3
|
+
[](http://rubydoc.info/github/maximgurin/verifica)
|
2
4
|
[](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com&utm_medium=referral&utm_content=maximgurin/verifica&utm_campaign=Badge_Grade)
|
3
5
|
[](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com&utm_medium=referral&utm_content=maximgurin/verifica&utm_campaign=Badge_Coverage)
|
4
6
|

|
@@ -18,11 +20,11 @@ for any user and resource, regardless of how complex the authorization rules are
|
|
18
20
|
|
19
21
|
*Note: Verifica is a new open-source gem, so you may wonder if it's reliable. Internally,
|
20
22
|
this solution has been battle-tested in several B2B products, including one with over 15M database records.
|
21
|
-
But anyway
|
23
|
+
But DYOR anyway.*
|
22
24
|
|
23
25
|
## Why Verifica? Isn't Pundit or CanCanCan enough?
|
24
26
|
|
25
|
-
Let's say you working on a video platform application:
|
27
|
+
Let's say you are working on a video platform application:
|
26
28
|
|
27
29
|
- You have 10M videos in the database
|
28
30
|
- 7 types of user roles
|
@@ -36,7 +38,7 @@ In the [Real-world example with Rails](#real-world-example-with-rails) you can s
|
|
36
38
|
## Basic example
|
37
39
|
|
38
40
|
```ruby
|
39
|
-
require
|
41
|
+
require "verifica"
|
40
42
|
|
41
43
|
User = Struct.new(:id, :role, keyword_init: true) do
|
42
44
|
# Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids
|
@@ -70,26 +72,32 @@ authorizer = Verifica.authorizer do |config|
|
|
70
72
|
end
|
71
73
|
|
72
74
|
public_video = Video.new(id: 1, author_id: 1000, public: true)
|
73
|
-
private_video = Video.new(id: 2, author_id: 1000, public:
|
75
|
+
private_video = Video.new(id: 2, author_id: 1000, public: false)
|
74
76
|
|
75
77
|
superuser = User.new(id: 777, role: "root")
|
76
78
|
video_author = User.new(id: 1000, role: "user")
|
77
79
|
other_user = User.new(id: 2000, role: "user")
|
78
80
|
|
79
|
-
authorizer.authorized?(superuser, private_video, :delete)
|
80
|
-
# true
|
81
|
-
|
82
|
-
authorizer.authorized?(
|
83
|
-
# true
|
84
|
-
|
85
|
-
authorizer.authorized?(other_user, private_video, :read)
|
86
|
-
# false
|
81
|
+
authorizer.authorized?(superuser, private_video, :delete) # => true
|
82
|
+
authorizer.authorized?(video_author, private_video, :delete) # => true
|
83
|
+
authorizer.authorized?(other_user, private_video, :read) # => false
|
84
|
+
authorizer.authorized?(other_user, public_video, :comment) # => true
|
87
85
|
|
88
|
-
|
89
|
-
#
|
86
|
+
begin
|
87
|
+
# raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'
|
88
|
+
authorizer.authorize(other_user, public_video, :write)
|
89
|
+
rescue Verifica::AuthorizationError => e
|
90
|
+
e.explain # => Long-form explanation of why action is not authorized, your debugging friend
|
91
|
+
end
|
90
92
|
|
91
|
-
|
92
|
-
|
93
|
+
# #authorization_result returns a special object with a bunch of useful info
|
94
|
+
auth_result = authorizer.authorization_result(superuser, private_video, :delete)
|
95
|
+
auth_result.success? # => true
|
96
|
+
auth_result.subject_id # => 777
|
97
|
+
auth_result.resource_type # => :video
|
98
|
+
auth_result.action # => :delete
|
99
|
+
auth_result.allowed_actions # => [:read, :write, :delete, :comment]
|
100
|
+
auth_result.explain # => Long-form explanation of why action is authorized
|
93
101
|
```
|
94
102
|
|
95
103
|
## Installation
|
@@ -120,11 +128,11 @@ class User
|
|
120
128
|
def subject_id
|
121
129
|
123
|
122
130
|
end
|
123
|
-
|
131
|
+
|
124
132
|
def subject_type
|
125
133
|
:user
|
126
134
|
end
|
127
|
-
|
135
|
+
|
128
136
|
def subject_sids
|
129
137
|
["root"] # see Security Identifier section below to understand what is this for
|
130
138
|
end
|
@@ -143,7 +151,7 @@ class Post
|
|
143
151
|
def resource_id
|
144
152
|
1
|
145
153
|
end
|
146
|
-
|
154
|
+
|
147
155
|
def resource_type
|
148
156
|
:post
|
149
157
|
end
|
@@ -184,9 +192,9 @@ end
|
|
184
192
|
|
185
193
|
video_acl.to_a
|
186
194
|
# =>
|
187
|
-
# [#<Verifica::Ace:0x00007fab1955dd60 @action=:
|
188
|
-
# #<Verifica::Ace:0x00007fab1955dd10 @action=:comment, @allow=true, @sid="authenticated">,
|
189
|
-
# #<Verifica::Ace:0x00007fab1955dc48 @action=:
|
195
|
+
# [#<Verifica::Ace:0x00007fab1955dd60 @action=:read, @allow=true, @sid="authenticated">,
|
196
|
+
# #<Verifica::Ace:0x00007fab1955dd10 @action=:comment, @allow=true, @sid="authenticated">,
|
197
|
+
# #<Verifica::Ace:0x00007fab1955dc48 @action=:read, @allow=false, @sid="country:US">]
|
190
198
|
```
|
191
199
|
|
192
200
|
### AclProvider
|
@@ -210,8 +218,8 @@ end
|
|
210
218
|
### Authorizer
|
211
219
|
|
212
220
|
And finally, Authorizer, the heart of Verifica. It couples all concepts above into an isolated container with no global state.
|
213
|
-
Each Authorizer has a list of resource types registered with their companion AclProviders
|
214
|
-
|
221
|
+
Each Authorizer has a list of resource types registered with their companion AclProviders and
|
222
|
+
several methods to check the Subject's rights to perform a specific action on a given resource.
|
215
223
|
|
216
224
|
Check the [Basic example](#basic-example) above to see how it all plays together.
|
217
225
|
|
@@ -280,7 +288,7 @@ class VideoAclProvider
|
|
280
288
|
author_org = video.author.organization
|
281
289
|
allowed_countries = author_org&.allow_countries || ds.allow_countries
|
282
290
|
denied_countries = author_org&.deny_countries || ds.deny_countries
|
283
|
-
|
291
|
+
|
284
292
|
# ...and 30 more lines to handle all our requirements
|
285
293
|
end
|
286
294
|
end
|
@@ -318,11 +326,11 @@ class User < ApplicationRecord
|
|
318
326
|
when "moderator"
|
319
327
|
[user_sid(id), role_sid("moderator")]
|
320
328
|
when "user"
|
321
|
-
sids = [authenticated_sid, user_sid(id),
|
329
|
+
sids = [authenticated_sid, user_sid(id), country_sid(country)]
|
322
330
|
organization_id.try { |org_id| sids.push(organization_sid(org_id)) }
|
323
331
|
sids
|
324
332
|
when "organization_admin"
|
325
|
-
sids = [authenticated_sid, user_sid(id),
|
333
|
+
sids = [authenticated_sid, user_sid(id), country_sid(country)]
|
326
334
|
sids.push(organization_sid(organization_id))
|
327
335
|
sids.push(role_sid("organization_admin:#{organization_id}"))
|
328
336
|
else
|
@@ -354,7 +362,7 @@ end
|
|
354
362
|
```
|
355
363
|
|
356
364
|
```ruby
|
357
|
-
# app/models/
|
365
|
+
# app/models/video.rb
|
358
366
|
|
359
367
|
class Video < ApplicationRecord
|
360
368
|
attr_accessor :allowed_actions
|
@@ -363,7 +371,7 @@ class Video < ApplicationRecord
|
|
363
371
|
before_save :update_read_acl
|
364
372
|
|
365
373
|
def resource_type = :video
|
366
|
-
|
374
|
+
|
367
375
|
def update_read_acl
|
368
376
|
acl = AUTHORIZER.resource_acl(self)
|
369
377
|
self.read_allow_sids = acl.allowed_sids(:read)
|
@@ -383,12 +391,16 @@ end
|
|
383
391
|
|
384
392
|
class VideosController
|
385
393
|
def index
|
386
|
-
@videos = Video
|
394
|
+
@videos = Video
|
395
|
+
.includes(:distribution_setting, author: [:organization])
|
396
|
+
.available_for(current_user)
|
397
|
+
.order(:name)
|
398
|
+
.limit(50)
|
387
399
|
end
|
388
|
-
|
400
|
+
|
389
401
|
def show
|
390
402
|
@video = Video.find(params[:id])
|
391
|
-
|
403
|
+
|
392
404
|
# upon successful authorization helper object is returned with a bunch of useful info
|
393
405
|
auth_result = AUTHORIZER.authorize(current_user, @video, :read)
|
394
406
|
|
@@ -415,8 +427,6 @@ run a background job to find all affected videos and update `read_allow_sids`, `
|
|
415
427
|
Same applies to Distribution Settings and other dependencies.
|
416
428
|
- **Rules change handling.** If implementation of `VideoAclProvider` changed you need to run a background job
|
417
429
|
to update `read_allow_sids`, `read_deny_sids` columns for all videos.
|
418
|
-
- **Cache, N+1 problem.** `VideoAclProvider` retrieves a chain of records associated with each video which leads to
|
419
|
-
N+1 problem in a naive implementation.
|
420
430
|
|
421
431
|
See also:
|
422
432
|
|
data/lib/verifica/acl.rb
CHANGED
@@ -54,7 +54,6 @@ module Verifica
|
|
54
54
|
|
55
55
|
allow_deny[:allowed_sids].freeze
|
56
56
|
allow_deny[:denied_sids].freeze
|
57
|
-
allow_deny.freeze
|
58
57
|
end
|
59
58
|
|
60
59
|
@allowed_actions.freeze
|
@@ -154,7 +153,7 @@ module Verifica
|
|
154
153
|
|
155
154
|
# @example
|
156
155
|
# acl = Verifica::Acl.build { |acl| acl.allow "root", [:read, :write] }
|
157
|
-
# acl.to_a.map(
|
156
|
+
# acl.to_a.map(&:to_h)
|
158
157
|
# # => [{:sid=>"root", :action=>:read, :allow=>true}, {:sid=>"root", :action=>:write, :allow=>true}]
|
159
158
|
#
|
160
159
|
# @return [Array<Ace>] a new array representing +self+
|
data/lib/verifica/authorizer.rb
CHANGED
@@ -73,6 +73,11 @@ module Verifica
|
|
73
73
|
|
74
74
|
# The same as {#authorize} but returns true/false instead of rising an exception
|
75
75
|
#
|
76
|
+
# @param subject (see #authorize)
|
77
|
+
# @param resource (see #authorize)
|
78
|
+
# @param action (see #authorize)
|
79
|
+
# @param context (see #authorize)
|
80
|
+
#
|
76
81
|
# @return [Boolean] true if +action+ on +resource+ is authorized for +subject+
|
77
82
|
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
78
83
|
#
|
@@ -81,9 +86,31 @@ module Verifica
|
|
81
86
|
authorization_result(subject, resource, action, **context).success?
|
82
87
|
end
|
83
88
|
|
84
|
-
#
|
85
|
-
#
|
86
|
-
# @param
|
89
|
+
# The same as {#authorize} but returns a special result object instead of rising an exception
|
90
|
+
#
|
91
|
+
# @param subject (see #authorize)
|
92
|
+
# @param resource (see #authorize)
|
93
|
+
# @param action (see #authorize)
|
94
|
+
# @param context (see #authorize)
|
95
|
+
#
|
96
|
+
# @return [AuthorizationResult] authorization result with all details
|
97
|
+
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def authorization_result(subject, resource, action, **context)
|
101
|
+
action = action.to_sym
|
102
|
+
possible_actions = config_by_resource(resource).possible_actions
|
103
|
+
unless possible_actions.include?(action)
|
104
|
+
raise Error, "'#{action}' action is not registered as possible for '#{resource.resource_type}' resource"
|
105
|
+
end
|
106
|
+
|
107
|
+
acl = resource_acl(resource, **context)
|
108
|
+
AuthorizationResult.new(subject, resource, action, acl, **context)
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param subject (see #authorize)
|
112
|
+
# @param resource (see #authorize)
|
113
|
+
# @param context (see #authorize)
|
87
114
|
#
|
88
115
|
# @return [Array<Symbol>] array of actions allowed for +subject+ or empty array if none
|
89
116
|
# @raise [Error] if +resource.resource_type+ isn't registered in +self+
|
@@ -128,7 +155,7 @@ module Verifica
|
|
128
155
|
@resources.key?(resource_type.to_sym)
|
129
156
|
end
|
130
157
|
|
131
|
-
# @param resource
|
158
|
+
# @param resource (see #authorize)
|
132
159
|
# @param context [Hash] arbitrary keyword arguments to forward to +acl_provider.call+
|
133
160
|
#
|
134
161
|
# @return [Acl] Access Control List for +resource+
|
@@ -141,6 +168,7 @@ module Verifica
|
|
141
168
|
def resource_acl(resource, **context)
|
142
169
|
config = config_by_resource(resource)
|
143
170
|
acl = config.acl_provider.call(resource, **context)
|
171
|
+
# trade-off flexibility to increase robustness here by requiring a specific type
|
144
172
|
unless acl.is_a?(Verifica::Acl)
|
145
173
|
type = resource.resource_type
|
146
174
|
raise Error, "'#{type}' resource acl_provider should respond to #call with Acl object but got '#{acl.class}'"
|
@@ -172,16 +200,5 @@ module Verifica
|
|
172
200
|
|
173
201
|
resource_config(type)
|
174
202
|
end
|
175
|
-
|
176
|
-
private def authorization_result(subject, resource, action, **context)
|
177
|
-
action = action.to_sym
|
178
|
-
possible_actions = config_by_resource(resource).possible_actions
|
179
|
-
unless possible_actions.include?(action)
|
180
|
-
raise Error, "'#{action}' action is not registered as possible for '#{resource.resource_type}' resource"
|
181
|
-
end
|
182
|
-
|
183
|
-
acl = resource_acl(resource, **context)
|
184
|
-
AuthorizationResult.new(subject, resource, action, acl, **context)
|
185
|
-
end
|
186
203
|
end
|
187
204
|
end
|
data/lib/verifica/sid.rb
CHANGED
@@ -211,5 +211,66 @@ module Verifica
|
|
211
211
|
|
212
212
|
"org:#{organization_id}".freeze
|
213
213
|
end
|
214
|
+
|
215
|
+
# Security Identifier of the subject who is a member of the group with given +group_id+
|
216
|
+
#
|
217
|
+
# @note (see #user_sid)
|
218
|
+
#
|
219
|
+
# @example
|
220
|
+
# class PostAclProvider
|
221
|
+
# include Verifica::Sid
|
222
|
+
#
|
223
|
+
# def call(post, **)
|
224
|
+
# Verifica::Acl.build do |acl|
|
225
|
+
# post.editor_groups.each do |group_id|
|
226
|
+
# acl.allow group_sid(group_id), [:edit, :delete]
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# # ...
|
230
|
+
# end
|
231
|
+
# end
|
232
|
+
# end
|
233
|
+
#
|
234
|
+
# @return [String]
|
235
|
+
#
|
236
|
+
# @api public
|
237
|
+
def group_sid(group_id)
|
238
|
+
if group_id.nil?
|
239
|
+
raise ArgumentError, "Nil 'group_id' is unsafe. Use empty string if you absolutely need this behavior"
|
240
|
+
end
|
241
|
+
|
242
|
+
"group:#{group_id}".freeze
|
243
|
+
end
|
244
|
+
|
245
|
+
# Security Identifier of the subject whose country is the country with given +country_id+
|
246
|
+
#
|
247
|
+
# @note (see #user_sid)
|
248
|
+
#
|
249
|
+
# @example
|
250
|
+
# class PostAclProvider
|
251
|
+
# include Verifica::Sid
|
252
|
+
#
|
253
|
+
# def call(post, **)
|
254
|
+
# Verifica::Acl.build do |acl|
|
255
|
+
# acl.allow authenticated_sid, [:read, :comment]
|
256
|
+
# post.banned_countries.each do |country_id|
|
257
|
+
# acl.deny country_sid(country_id), [:read, :comment]
|
258
|
+
# end
|
259
|
+
#
|
260
|
+
# # ...
|
261
|
+
# end
|
262
|
+
# end
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# @return [String]
|
266
|
+
#
|
267
|
+
# @api public
|
268
|
+
def country_sid(country_id)
|
269
|
+
if country_id.nil?
|
270
|
+
raise ArgumentError, "Nil 'country_id' is unsafe. Use empty string if you absolutely need this behavior"
|
271
|
+
end
|
272
|
+
|
273
|
+
"country:#{country_id}".freeze
|
274
|
+
end
|
214
275
|
end
|
215
276
|
end
|
data/lib/verifica/version.rb
CHANGED
data/lib/verifica.rb
CHANGED
@@ -20,7 +20,7 @@ require_relative "verifica/version"
|
|
20
20
|
# (who can do what for any given resource) and execution (can +current_user+ delete this post?).
|
21
21
|
#
|
22
22
|
# @example
|
23
|
-
# require
|
23
|
+
# require "verifica"
|
24
24
|
#
|
25
25
|
# User = Struct.new(:id, :role, keyword_init: true) do
|
26
26
|
# # Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids
|
@@ -54,26 +54,32 @@ require_relative "verifica/version"
|
|
54
54
|
# end
|
55
55
|
#
|
56
56
|
# public_video = Video.new(id: 1, author_id: 1000, public: true)
|
57
|
-
# private_video = Video.new(id: 2, author_id: 1000, public:
|
57
|
+
# private_video = Video.new(id: 2, author_id: 1000, public: false)
|
58
58
|
#
|
59
59
|
# superuser = User.new(id: 777, role: "root")
|
60
60
|
# video_author = User.new(id: 1000, role: "user")
|
61
61
|
# other_user = User.new(id: 2000, role: "user")
|
62
62
|
#
|
63
|
-
# authorizer.authorized?(superuser, private_video, :delete)
|
64
|
-
# # true
|
63
|
+
# authorizer.authorized?(superuser, private_video, :delete) # => true
|
64
|
+
# authorizer.authorized?(video_author, private_video, :delete) # => true
|
65
|
+
# authorizer.authorized?(other_user, private_video, :read) # => false
|
66
|
+
# authorizer.authorized?(other_user, public_video, :comment) # => true
|
65
67
|
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# authorizer.authorized?(other_user, public_video, :comment)
|
73
|
-
# # true
|
68
|
+
# begin
|
69
|
+
# # raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'
|
70
|
+
# authorizer.authorize(other_user, public_video, :write)
|
71
|
+
# rescue Verifica::AuthorizationError => e
|
72
|
+
# e.explain # => Long-form explanation of why action is not authorized, your debugging friend
|
73
|
+
# end
|
74
74
|
#
|
75
|
-
#
|
76
|
-
#
|
75
|
+
# # #authorization_result returns a special object with a bunch of useful info
|
76
|
+
# auth_result = authorizer.authorization_result(superuser, private_video, :delete)
|
77
|
+
# auth_result.success? # => true
|
78
|
+
# auth_result.subject_id # => 777
|
79
|
+
# auth_result.resource_type # => :video
|
80
|
+
# auth_result.action # => :delete
|
81
|
+
# auth_result.allowed_actions # => [:read, :write, :delete, :comment]
|
82
|
+
# auth_result.explain # => Long-form explanation of why action is authorized
|
77
83
|
#
|
78
84
|
# @api public
|
79
85
|
module Verifica
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: verifica
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maxim Gurin
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -105,7 +105,7 @@ metadata:
|
|
105
105
|
source_code_uri: https://github.com/maximgurin/verifica
|
106
106
|
bug_tracker_uri: https://github.com/maximgurin/verifica/issues
|
107
107
|
rubygems_mfa_required: 'true'
|
108
|
-
post_install_message:
|
108
|
+
post_install_message:
|
109
109
|
rdoc_options: []
|
110
110
|
require_paths:
|
111
111
|
- lib
|
@@ -120,8 +120,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
120
|
- !ruby/object:Gem::Version
|
121
121
|
version: '0'
|
122
122
|
requirements: []
|
123
|
-
rubygems_version: 3.4.
|
124
|
-
signing_key:
|
123
|
+
rubygems_version: 3.4.19
|
124
|
+
signing_key:
|
125
125
|
specification_version: 4
|
126
126
|
summary: The most scalable authorization solution for Ruby
|
127
127
|
test_files: []
|