verifica 1.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +449 -0
- data/lib/verifica/ace.rb +79 -0
- data/lib/verifica/acl.rb +208 -0
- data/lib/verifica/acl_builder.rb +63 -0
- data/lib/verifica/authorization_result.rb +152 -0
- data/lib/verifica/authorizer.rb +187 -0
- data/lib/verifica/configuration.rb +41 -0
- data/lib/verifica/errors.rb +84 -0
- data/lib/verifica/resource_configuration.rb +57 -0
- data/lib/verifica/sid.rb +215 -0
- data/lib/verifica/version.rb +5 -0
- data/lib/verifica.rb +122 -0
- data/verifica.gemspec +41 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8df98ff228b89e7701f41df3d5617d93183e83c32cdae5c879e347c615f367d7
|
4
|
+
data.tar.gz: 956259725d32e5a6208a03f2a07d682d6facbb1dcaae107687ce59cd1cd8363c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3329c9e126220d43a8e9b4a7923541b30139e74f565387bed6012acea9760239d1b5850d19ee2963e63252ddde3008e39486ac66438e6e9215afe18de0d4256
|
7
|
+
data.tar.gz: 1dab1e7bab877e5f63298240db13270c6f46c238643f508037e81f524a99a54f3613e3515690e54886ac032b371d394622e51d5d33d92528ddab8560d7babb2e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [1.0.0] - 2023-01-19
|
11
|
+
|
12
|
+
Initial public release
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022-2023 Maxim Gurin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,449 @@
|
|
1
|
+
[](https://github.com/maximgurin/verifica/actions/workflows/ci.yml)
|
2
|
+
[](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com&utm_medium=referral&utm_content=maximgurin/verifica&utm_campaign=Badge_Grade)
|
3
|
+
[](https://www.codacy.com/gh/maximgurin/verifica/dashboard?utm_source=github.com&utm_medium=referral&utm_content=maximgurin/verifica&utm_campaign=Badge_Coverage)
|
4
|
+

|
5
|
+
|
6
|
+
# Verifica
|
7
|
+
|
8
|
+
Verifica is Ruby's most scalable authorization solution ready to handle sophisticated authorization rules.
|
9
|
+
|
10
|
+
- Framework and database agnostic
|
11
|
+
- Scalable. Start from 10, grow to 10M records in the database while having the same authorization architecture
|
12
|
+
- Supports any actor in your application. Traditional `current_user`, external service, API client, you name it
|
13
|
+
- No global state. Only local, immutable objects
|
14
|
+
- Plain old Ruby, zero dependencies, no magic
|
15
|
+
|
16
|
+
Verifica is designed around Access Control List. ACL powers a straightforward and unified authorization flow
|
17
|
+
for any user and resource, regardless of how complex the authorization rules are.
|
18
|
+
|
19
|
+
*Note: Verifica is a new open-source gem, so you may wonder if it's reliable. Internally,
|
20
|
+
this solution has been battle-tested in several B2B products, including one with over 15M database records.
|
21
|
+
But anyway, trust nothing. DYOR.*
|
22
|
+
|
23
|
+
## Why Verifica? Isn't Pundit or CanCanCan enough?
|
24
|
+
|
25
|
+
Let's say you working on a video platform application:
|
26
|
+
|
27
|
+
- You have 10M videos in the database
|
28
|
+
- 7 types of user roles
|
29
|
+
- 20 rules defining who is allowed to access the video
|
30
|
+
- Rules require querying other entities too (video author settings, author's organization settings, etc.)
|
31
|
+
|
32
|
+
Given all these, *how do you even find a list of videos available for `current_user`?*
|
33
|
+
Bunch of `if/elsif` and enormous SQL query with many joins? Is there a better way? Verifica shines for this kind of problem.
|
34
|
+
In the [Real-world example with Rails](#real-world-example-with-rails) you can see the solution in detail.
|
35
|
+
|
36
|
+
## Basic example
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'verifica'
|
40
|
+
|
41
|
+
User = Struct.new(:id, :role, keyword_init: true) do
|
42
|
+
# Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids
|
43
|
+
alias_method :subject_id, :id
|
44
|
+
def subject_type = :user
|
45
|
+
|
46
|
+
def subject_sids(**)
|
47
|
+
role == "root" ? ["root"] : ["authenticated", "user:#{id}"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Video = Struct.new(:id, :author_id, :public, keyword_init: true) do
|
52
|
+
# Verifica expects each secured resource to respond to #resource_id, and #resource_type
|
53
|
+
alias_method :resource_id, :id
|
54
|
+
def resource_type = :video
|
55
|
+
end
|
56
|
+
|
57
|
+
video_acl_provider = lambda do |video, **|
|
58
|
+
Verifica::Acl.build do |acl|
|
59
|
+
acl.allow "root", [:read, :write, :delete, :comment]
|
60
|
+
acl.allow "user:#{video.author_id}", [:read, :write, :delete, :comment]
|
61
|
+
|
62
|
+
if video.public
|
63
|
+
acl.allow "authenticated", [:read, :comment]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
authorizer = Verifica.authorizer do |config|
|
69
|
+
config.register_resource :video, [:read, :write, :delete, :comment], video_acl_provider
|
70
|
+
end
|
71
|
+
|
72
|
+
public_video = Video.new(id: 1, author_id: 1000, public: true)
|
73
|
+
private_video = Video.new(id: 2, author_id: 1000, public: true)
|
74
|
+
|
75
|
+
superuser = User.new(id: 777, role: "root")
|
76
|
+
video_author = User.new(id: 1000, role: "user")
|
77
|
+
other_user = User.new(id: 2000, role: "user")
|
78
|
+
|
79
|
+
authorizer.authorized?(superuser, private_video, :delete)
|
80
|
+
# true
|
81
|
+
|
82
|
+
authorizer.authorized?(video_author, private_video, :delete)
|
83
|
+
# true
|
84
|
+
|
85
|
+
authorizer.authorized?(other_user, private_video, :read)
|
86
|
+
# false
|
87
|
+
|
88
|
+
authorizer.authorized?(other_user, public_video, :comment)
|
89
|
+
# true
|
90
|
+
|
91
|
+
authorizer.authorize(other_user, public_video, :write)
|
92
|
+
# raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'
|
93
|
+
```
|
94
|
+
|
95
|
+
## Installation
|
96
|
+
|
97
|
+
**Required Ruby version >= 3.0**
|
98
|
+
|
99
|
+
Install the gem and add to the application's Gemfile by executing:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
$ bundle add verifica
|
103
|
+
```
|
104
|
+
|
105
|
+
## Core concepts
|
106
|
+
|
107
|
+
Get a high-level overview of Verifica's core concepts and architecture before diving into usage nuances.
|
108
|
+
Verifica may appear complex initially, but it prioritizes explicitness, flexibility, and scalability over nice looking magic.
|
109
|
+
Here is an explanation of each component:
|
110
|
+
|
111
|
+
### Subject
|
112
|
+
|
113
|
+
Security subject is a user, process, or system granted access to specific resources.
|
114
|
+
In most applications the subject is currently authenticated user, aka `current_user`.
|
115
|
+
|
116
|
+
In code a subject could be represented by any object that responds to `#subject_id`, `#subject_type`, and `#subject_sids`.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class User
|
120
|
+
def subject_id
|
121
|
+
123
|
122
|
+
end
|
123
|
+
|
124
|
+
def subject_type
|
125
|
+
:user
|
126
|
+
end
|
127
|
+
|
128
|
+
def subject_sids
|
129
|
+
["root"] # see Security Identifier section below to understand what is this for
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
### Resource
|
135
|
+
|
136
|
+
Resource refers to anything that requires protection.
|
137
|
+
In most applications resources are entities stored in the database, such as Post, Comment, User, etc.
|
138
|
+
|
139
|
+
In code a resource could be represented by any object that responds to `#resource_id` and `#resource_type`.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class Post
|
143
|
+
def resource_id
|
144
|
+
1
|
145
|
+
end
|
146
|
+
|
147
|
+
def resource_type
|
148
|
+
:post
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Action
|
154
|
+
|
155
|
+
Action that Subject can perform on a protected Resource. Represented as a Symbol in code,
|
156
|
+
it could be traditional `:read`, `:write`, `:delete` or more domain specific `:comment`, `:publish`, etc.
|
157
|
+
|
158
|
+
### Security Identifier
|
159
|
+
|
160
|
+
SID is a value used to identify and differentiate Subjects
|
161
|
+
and assign access rights based on the subject's attributes like role, organization, group, or country.
|
162
|
+
|
163
|
+
In code SID could be represented by immutable string (other objects work too, equality check is the only requirement).
|
164
|
+
Each subject has one or more SIDs.
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
superuser.subject_sids # => ["root"]
|
168
|
+
moderator_user.subject_sids # => ["user:321", "role:moderator"]
|
169
|
+
regular_user.subject_sids # => ["authenticated", "user:123", "country:UA"]
|
170
|
+
organization_user.subject_sids # => ["authenticated", "user:456", "country:UA", "org:789"]
|
171
|
+
anonymous_user.subject_sids # => ["anonymous", "country:UA"]
|
172
|
+
```
|
173
|
+
|
174
|
+
### Access Control List
|
175
|
+
|
176
|
+
ACL consists of Access Control Entries (ACEs) and defines which actions are allowed or denied for particular SIDs.
|
177
|
+
ACL is associated with a specific protected resource in your system.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
video_acl = Verifica::Acl.build do |acl|
|
181
|
+
acl.allow "authenticated", [:read, :comment]
|
182
|
+
acl.deny "country:US", [:read]
|
183
|
+
end
|
184
|
+
|
185
|
+
video_acl.to_a
|
186
|
+
# =>
|
187
|
+
# [#<Verifica::Ace:0x00007fab1955dd60 @action=:view, @allow=true, @sid="authenticated">,
|
188
|
+
# #<Verifica::Ace:0x00007fab1955dd10 @action=:comment, @allow=true, @sid="authenticated">,
|
189
|
+
# #<Verifica::Ace:0x00007fab1955dc48 @action=:view, @allow=false, @sid="country:US">]
|
190
|
+
```
|
191
|
+
|
192
|
+
### AclProvider
|
193
|
+
|
194
|
+
AclProvider is an object that responds to `#call(resource, **)` and returns ACL for the given resource.
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
class VideoAclProvider
|
198
|
+
def call(video, **context)
|
199
|
+
Verifica::Acl.build do |acl|
|
200
|
+
acl.allow "user:#{video.author_id}", [:read, :write, :delete, :comment]
|
201
|
+
|
202
|
+
if video.public?
|
203
|
+
acl.allow "authenticated", [:read, :comment]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
### Authorizer
|
211
|
+
|
212
|
+
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
|
+
And most importantly, Authorizer has several methods to check the Subject's rights to perform a specific action on a given resource.
|
215
|
+
|
216
|
+
Check the [Basic example](#basic-example) above to see how it all plays together.
|
217
|
+
|
218
|
+
## Real-world example with Rails
|
219
|
+
|
220
|
+
Demo: https://verifica-rails-example.maximgurin.com
|
221
|
+
|
222
|
+
Let's say you started working on your *next big thing* idea — a video hosting application.
|
223
|
+
In the beginning, you have only 2 user types and straightforward rules:
|
224
|
+
|
225
|
+
- *Admins* can see all videos
|
226
|
+
- *Users* can see their own videos and public videos of other users
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class Video
|
230
|
+
scope :available_for, ->(user) do
|
231
|
+
where(public: true).or(where(author_id: user.id)) unless user.admin?
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class VideosController
|
236
|
+
def index
|
237
|
+
@videos = Video.available_for(current_user)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
```
|
241
|
+
|
242
|
+
Time goes by and 4 years later you have:
|
243
|
+
|
244
|
+
- 10M records in the videos table. Organization and personal user accounts
|
245
|
+
- 4 roles: *Admin*, *Moderator*, *Organization Admin*, *User*
|
246
|
+
- Video drafts available only to their authors
|
247
|
+
- Internal videos available only for members of the author's organization
|
248
|
+
- Country restrictions, either in the *allowlist* or *denylist* modes
|
249
|
+
- Distribution Settings entity with one-to-many relation to Videos
|
250
|
+
- Distribution mode: *public*, *internal*, or *private*
|
251
|
+
- Countries *allowlist* or *denylist*
|
252
|
+
- Organization-wide country restrictions overrides Distribution Settings
|
253
|
+
- *Organization Admins* can see private videos of their org members
|
254
|
+
- *Admins* and *Moderators* can see all videos, regardless of country restrictions
|
255
|
+
|
256
|
+
Wow, that's a pretty extensive list of requirements. Easy to get lost!
|
257
|
+
Now the most exciting part. How do you implement `Video.available_for` method with so many details to consider?
|
258
|
+
Videos table is big, so you can't use SQL joins to, let's say, check the video author's organization or other dependencies.
|
259
|
+
And even if you can, a query with so many joins and conditions would be write-only anyway :)
|
260
|
+
|
261
|
+
Here is how this challenge could be resolved using Verifica and ACL:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
# app/acl_providers/video_acl_provider.rb
|
265
|
+
|
266
|
+
class VideoAclProvider
|
267
|
+
include Verifica::Sid
|
268
|
+
|
269
|
+
POSSIBLE_ACTIONS = [:read, :write, :delete].freeze
|
270
|
+
|
271
|
+
def call(video, **)
|
272
|
+
Verifica::Acl.build do |acl|
|
273
|
+
acl.allow root_sid, POSSIBLE_ACTIONS
|
274
|
+
acl.allow user_sid(video.author_id), POSSIBLE_ACTIONS
|
275
|
+
acl.allow role_sid("moderator"), [:read, :delete]
|
276
|
+
|
277
|
+
next if video.draft?
|
278
|
+
|
279
|
+
ds = video.distribution_setting
|
280
|
+
author_org = video.author.organization
|
281
|
+
allowed_countries = author_org&.allow_countries || ds.allow_countries
|
282
|
+
denied_countries = author_org&.deny_countries || ds.deny_countries
|
283
|
+
|
284
|
+
# ...and 30 more lines to handle all our requirements
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
# config/initializers/verifica.rb
|
292
|
+
|
293
|
+
require "verifica"
|
294
|
+
|
295
|
+
# Quick and dirty way for simplicity
|
296
|
+
# In the real app, you could use DI container to hold configured Verifica::Authorizer instance
|
297
|
+
Rails.configuration.after_initialize do
|
298
|
+
AUTHORIZER = Verifica.authorizer do |config|
|
299
|
+
config.register_resource :video, VideoAclProvider::POSSIBLE_ACTIONS, VideoAclProvider.new
|
300
|
+
end
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
# app/models/user.rb
|
306
|
+
|
307
|
+
class User < ApplicationRecord
|
308
|
+
include Verifica::Sid
|
309
|
+
|
310
|
+
alias_method :subject_id, :id
|
311
|
+
|
312
|
+
def subject_type = :user
|
313
|
+
|
314
|
+
def subject_sids(**)
|
315
|
+
case role
|
316
|
+
when "root"
|
317
|
+
[root_sid]
|
318
|
+
when "moderator"
|
319
|
+
[user_sid(id), role_sid("moderator")]
|
320
|
+
when "user"
|
321
|
+
sids = [authenticated_sid, user_sid(id), "country:#{country}"]
|
322
|
+
organization_id.try { |org_id| sids.push(organization_sid(org_id)) }
|
323
|
+
sids
|
324
|
+
when "organization_admin"
|
325
|
+
sids = [authenticated_sid, user_sid(id), "country:#{country}"]
|
326
|
+
sids.push(organization_sid(organization_id))
|
327
|
+
sids.push(role_sid("organization_admin:#{organization_id}"))
|
328
|
+
else
|
329
|
+
throw RuntimeError("Unsupported user role: #{role}")
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
What we've done:
|
336
|
+
|
337
|
+
- Configured `Verifica::Authorizer` object. It's available as `AUTHORIZER` constant anywhere in the app
|
338
|
+
- Registered `:video` type as a secured resource. `VideoAclProvider` defines rules, who can do what
|
339
|
+
- Configured `User` to be a security Subject. Each user has list of Security Identifiers depending on the role and other attributes
|
340
|
+
|
341
|
+
Now, a few last steps and the challenge resolved:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
# db/migrate/20230113203815_add_read_sids_to_videos.rb
|
345
|
+
|
346
|
+
# For simplicity, we are adding two String array columns directly to videos table.
|
347
|
+
# In the real app, you could use something like ElasticSearch to hold videos with these companion columns
|
348
|
+
class AddReadSidsToVideos < ActiveRecord::Migration[7.0]
|
349
|
+
def change
|
350
|
+
add_column :videos, :read_allow_sids, :string, null: false, array: true, default: [], index: true
|
351
|
+
add_column :videos, :read_deny_sids, :string, null: false, array: true, default: [], index: true
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
# app/models/user.rb
|
358
|
+
|
359
|
+
class Video < ApplicationRecord
|
360
|
+
attr_accessor :allowed_actions
|
361
|
+
alias_method :resource_id, :id
|
362
|
+
|
363
|
+
before_save :update_read_acl
|
364
|
+
|
365
|
+
def resource_type = :video
|
366
|
+
|
367
|
+
def update_read_acl
|
368
|
+
acl = AUTHORIZER.resource_acl(self)
|
369
|
+
self.read_allow_sids = acl.allowed_sids(:read)
|
370
|
+
self.read_deny_sids = acl.denied_sids(:read)
|
371
|
+
end
|
372
|
+
|
373
|
+
# And finally, this is our goal. Straightforward implementation regardless of how complex the rules are.
|
374
|
+
scope :available_for, ->(user) do
|
375
|
+
sids = user.subject_sids
|
376
|
+
where("read_allow_sids && ARRAY[?]::varchar[]", sids).where.not("read_deny_sids && ARRAY[?]::varchar[]", sids)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
# app/controllers/videos_controller
|
383
|
+
|
384
|
+
class VideosController
|
385
|
+
def index
|
386
|
+
@videos = Video.available_for(current_user).order(:name).limit(50)
|
387
|
+
end
|
388
|
+
|
389
|
+
def show
|
390
|
+
@video = Video.find(params[:id])
|
391
|
+
|
392
|
+
# upon successful authorization helper object is returned with a bunch of useful info
|
393
|
+
auth_result = AUTHORIZER.authorize(current_user, @video, :read)
|
394
|
+
|
395
|
+
# add list of allowed actions so the frontend knows whether show "Edit" and "Delete" buttons, for example
|
396
|
+
@video.allowed_actions = auth_result.allowed_actions
|
397
|
+
end
|
398
|
+
|
399
|
+
def destroy
|
400
|
+
video = Video.find(params[:id])
|
401
|
+
AUTHORIZER.authorize(current_user, video, :delete)
|
402
|
+
video.destroy
|
403
|
+
end
|
404
|
+
end
|
405
|
+
```
|
406
|
+
|
407
|
+
Voila, we're done! So now, no matter how sophisticated our authorization rules are,
|
408
|
+
we have a clear method to find available videos for any user.
|
409
|
+
No conditions, no special handling for superusers as everyone goes through the unified mechanism.
|
410
|
+
|
411
|
+
**Important points not covered in this example but needed in the real app**:
|
412
|
+
|
413
|
+
- **Dependency change handling.** If country restrictions changed on the organization level you need to
|
414
|
+
run a background job to find all affected videos and update `read_allow_sids`, `read_deny_sids` columns.
|
415
|
+
Same applies to Distribution Settings and other dependencies.
|
416
|
+
- **Rules change handling.** If implementation of `VideoAclProvider` changed you need to run a background job
|
417
|
+
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
|
+
|
421
|
+
See also:
|
422
|
+
|
423
|
+
- Live demo - https://verifica-rails-example.maximgurin.com
|
424
|
+
- Full source code - https://github.com/maximgurin/verifica-rails-example
|
425
|
+
|
426
|
+
## Development
|
427
|
+
|
428
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
429
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
430
|
+
|
431
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
432
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
433
|
+
which will create a git tag for the version, push git commits and the created tag,
|
434
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
435
|
+
|
436
|
+
## Contributing
|
437
|
+
|
438
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/maximgurin/verifica.
|
439
|
+
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected
|
440
|
+
to adhere to the [code of conduct](https://github.com/maximgurin/verifica/blob/master/CODE_OF_CONDUCT.md).
|
441
|
+
|
442
|
+
## License
|
443
|
+
|
444
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
445
|
+
|
446
|
+
## Code of Conduct
|
447
|
+
|
448
|
+
Everyone interacting in the Verifica project's codebases, issue trackers, chat rooms and mailing lists is
|
449
|
+
expected to follow the [code of conduct](https://github.com/maximgurin/verifica/blob/master/CODE_OF_CONDUCT.md).
|
data/lib/verifica/ace.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verifica
|
4
|
+
# Access Control Entry (ACE)
|
5
|
+
#
|
6
|
+
# ACE is a minimal unit of the Access Control List (ACL) that defines whether or not a specific action
|
7
|
+
# is allowed for a particular Security Identifier (SID).
|
8
|
+
#
|
9
|
+
# @see Acl
|
10
|
+
# @see Sid
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
class Ace
|
14
|
+
# @return [String] Security Identifier (SID)
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
attr_reader :sid
|
18
|
+
|
19
|
+
# @return [Symbol] Action which is allowed or denied
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
attr_reader :action
|
23
|
+
|
24
|
+
# Creates a new Access Control Entry with immutable state
|
25
|
+
#
|
26
|
+
# @param sid [String] Security Identifier (SID), typically String,
|
27
|
+
# but could be any object with implemented equality methods and #hash
|
28
|
+
# @param action [Symbol, String] action which is allowed or denied for given SID
|
29
|
+
# @param allow [Boolean] allow or deny given action for given SID
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def initialize(sid, action, allow)
|
33
|
+
@sid = sid.dup.freeze
|
34
|
+
@action = action.to_sym
|
35
|
+
@allow = allow
|
36
|
+
freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] true if the action is allowed
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def allow?
|
43
|
+
@allow
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Boolean] true if the action is denied
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def deny?
|
50
|
+
!allow?
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Hash] a new hash representing +self+
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def to_h
|
57
|
+
{sid: @sid, action: @action, allow: @allow}
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
to_h.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
eql?(other)
|
66
|
+
end
|
67
|
+
|
68
|
+
def eql?(other)
|
69
|
+
self.class == other.class &&
|
70
|
+
@sid == other.sid &&
|
71
|
+
@action == other.action &&
|
72
|
+
@allow == other.allow?
|
73
|
+
end
|
74
|
+
|
75
|
+
def hash
|
76
|
+
[self.class, @sid, @action, @allow].hash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|