consent 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/.gitignore +14 -6
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +9 -11
- data/Gemfile +5 -1
- data/Rakefile +9 -3
- data/app/models/concerns/consent/authorizable.rb +94 -0
- data/app/models/consent/application_record.rb +7 -0
- data/app/models/consent/history.rb +20 -0
- data/app/models/consent/permission.rb +71 -0
- data/config.ru +9 -0
- data/consent.gemspec +24 -20
- data/db/migrate/20211104225614_create_nitro_auth_authorization_permissions.rb +19 -0
- data/db/migrate/20220420135558_create_nitro_auth_authorization_histories.rb +15 -0
- data/doc/dependency_decisions.yml +3 -0
- data/docs/CHANGELOG.md +23 -0
- data/docs/README.md +355 -0
- data/lib/consent/ability.rb +113 -4
- data/lib/consent/dsl.rb +1 -0
- data/lib/consent/{railtie.rb → engine.rb} +11 -8
- data/lib/consent/model_additions.rb +64 -0
- data/lib/consent/permission_migration.rb +139 -0
- data/lib/consent/reloader.rb +6 -5
- data/lib/consent/rspec/consent_action.rb +7 -7
- data/lib/consent/rspec/consent_view.rb +8 -11
- data/lib/consent/rspec.rb +3 -3
- data/lib/consent/subject_coder.rb +29 -0
- data/lib/consent/symbol_adapter.rb +18 -0
- data/lib/consent/version.rb +1 -1
- data/lib/consent.rb +25 -13
- data/lib/generators/consent/permissions_generator.rb +5 -5
- data/mkdocs.yml +5 -0
- data/renovate.json +15 -2
- metadata +86 -34
- data/.rspec +0 -2
- data/.ruby-version +0 -1
- data/.travis.yml +0 -20
- data/LICENSE +0 -21
- data/README.md +0 -252
- data/TODO.md +0 -1
data/docs/README.md
ADDED
@@ -0,0 +1,355 @@
|
|
1
|
+
# Consent [](https://travis-ci.org/powerhome/consent)
|
2
|
+
|
3
|
+
## What is Consent
|
4
|
+
|
5
|
+
Consent makes defining permissions easier by providing a clean, concise DSL for authorization
|
6
|
+
so that all abilities do not have to be in your `Ability` class.
|
7
|
+
|
8
|
+
Also, Consent adds an `Authorizable` model, so that you can easily grant permissions to your
|
9
|
+
ActiveRecord models.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'consent'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install consent
|
26
|
+
|
27
|
+
Then, require the engine in your `application.rb`
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "active_record/railtie"
|
31
|
+
require "consent/engine"
|
32
|
+
```
|
33
|
+
|
34
|
+
If you wish to use the activerecord adapter (`accessible_by` and `accessible_through`), you must load `active_record/railtie` before loading the `consent/engine`.
|
35
|
+
|
36
|
+
### Install and run the migrations
|
37
|
+
|
38
|
+
Copy and execute the migrations:
|
39
|
+
|
40
|
+
$ rails consent_engine:install:migrations
|
41
|
+
$ rails db:migrate
|
42
|
+
|
43
|
+
This will create the `consent_histories` and `consent_permissions` tables. If you want to use a different table prefix, you should set `Consent.table_name_prefix =` before you execute the migrations. I.e.:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# config/initializers/consent.rb
|
47
|
+
|
48
|
+
require "consent"
|
49
|
+
|
50
|
+
Consent.table_name_prefix = "my_app_"
|
51
|
+
```
|
52
|
+
|
53
|
+
## Authorizable
|
54
|
+
|
55
|
+
To grant permissions, you need an authorizable model. For our example we'll call it `Role`:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Role < ApplicationRecord
|
59
|
+
include ::Consent::Authorizable
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
You can now grant permissions to role with `grant`, `grant_all`, and `grant_all!`:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
role = Role.new
|
67
|
+
role.grant subject: Project, action: :update, view: :department
|
68
|
+
# OR
|
69
|
+
role.grant_all({ project: { update: :department } })
|
70
|
+
# OR
|
71
|
+
role.grant_all({ project: { update: :department } }, replace: true) # to replace everything
|
72
|
+
# OR
|
73
|
+
role.grant_all!({ project: { update: :department } }, replace: true) # to grant and save
|
74
|
+
|
75
|
+
role.permissions
|
76
|
+
=> [#<Consent::Permission subject: Project, action: :update, view: :department>]
|
77
|
+
```
|
78
|
+
|
79
|
+
In the above example, we're granting `:department` view to perform `:update` in the `Project` subject.
|
80
|
+
|
81
|
+
You can now create a `Consent::Ability` using the permissions granted to the role:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
ability = Consent::Ability.new(user, permissions: role.permissions)
|
85
|
+
```
|
86
|
+
|
87
|
+
## Defining permissions and views
|
88
|
+
|
89
|
+
Generate permissions with the `consent:permissions` generator. I.e:
|
90
|
+
|
91
|
+
$ rails g consent:permissions Project "Our Projects"
|
92
|
+
create app/permissions/projects.rb
|
93
|
+
create spec/permissions/projects_spec.rb
|
94
|
+
|
95
|
+
This will generate the permission definition:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Consent.define Project, "Our Projects" do
|
99
|
+
#in this case, Project is the subject
|
100
|
+
# and `Our Projects` is the description that makes it clear to users
|
101
|
+
# what the subject is acting upon.
|
102
|
+
…
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
We can now define the `:update` action and a couple of different views:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Consent.define Project, "Our Projects" do
|
110
|
+
view :all, "All projects"
|
111
|
+
|
112
|
+
view :department, "Projects from their department" do |user|
|
113
|
+
{ department_id: user.department_id }
|
114
|
+
end
|
115
|
+
|
116
|
+
view :team, "Projects from their team" do |user|
|
117
|
+
{ team_id: user.team_id }
|
118
|
+
end
|
119
|
+
|
120
|
+
action :update, views: %i[department team all]
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
The `:department` view will restrict the user to projects with matching `department_id`. That
|
125
|
+
means that for `Project.accessible_by(ability, :update)`, with an ability using a User with
|
126
|
+
department_id = 13, it will run a query similar to:
|
127
|
+
|
128
|
+
```sql
|
129
|
+
> user = User.new(department_id: 13)
|
130
|
+
> ability = Consent::Ability.new(user)
|
131
|
+
> ability.consent subject: Project, action: :update, view: :department
|
132
|
+
> Project.accessible_by(user).to_sql
|
133
|
+
"SELECT * FROM projects WHERE department_id = 1"
|
134
|
+
```
|
135
|
+
|
136
|
+
### Subject
|
137
|
+
|
138
|
+
The subject is the central point of a group of actions and views. It will typically
|
139
|
+
be an `ActiveRecord`, a `:symbol`, or any plain ruby class.
|
140
|
+
|
141
|
+
### Views
|
142
|
+
|
143
|
+
Views are the rules that limit access to actions. For instance, a user may see a `Project`
|
144
|
+
from his department, but not from others. You can enforce it with a `:department` view,
|
145
|
+
as in the examples below:
|
146
|
+
|
147
|
+
### Hash Conditions
|
148
|
+
|
149
|
+
Probably the most commonly used. When the view can be defined using a `where` scope in
|
150
|
+
an ActiveRecord context. It follows a match condition and will return all objects that meet
|
151
|
+
the criteria:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
Consent.define Project, 'Projects' do
|
155
|
+
view :department, "User's department only" do |user|
|
156
|
+
{ department_id: user.id }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
Although hash conditions (matching object's attributes) are recommended, the constraints can
|
162
|
+
be anything you want. Since Consent does not enforce the rules, those rules are directly given
|
163
|
+
to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice)
|
164
|
+
for defining abilities is recommended.
|
165
|
+
|
166
|
+
### Object Conditions
|
167
|
+
|
168
|
+
If you're not matching for equal values, then you would need to use an object condition.
|
169
|
+
|
170
|
+
If you already have an object and want to check to see whether the user has permission to view
|
171
|
+
that specific object, you would use object conditions.
|
172
|
+
|
173
|
+
If your needs can't be satisfied by hash conditions, it is recommended that a second condition
|
174
|
+
is given for constraining object instances. For example, if you want to restrict a view for smaller
|
175
|
+
volume projects:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
Consent.define Project, 'Projects' do
|
179
|
+
view :small_volumes, "User's department only",
|
180
|
+
-> (user) {
|
181
|
+
['amount < ?', user.volume_limit]
|
182
|
+
end,
|
183
|
+
-> (user, project) {
|
184
|
+
project.amount < user.volume_limit
|
185
|
+
}
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
For object conditions, the latter argument will be the referred object, while the
|
190
|
+
first will be the context given to the [Permission](#permission) (also check
|
191
|
+
[CanCan integration](#cancan-integration)).
|
192
|
+
|
193
|
+
### Action
|
194
|
+
|
195
|
+
An action is anything you can perform on a given subject. In the example of
|
196
|
+
Features this would look like the following using Consent's DSL:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
Consent.define :features, 'Beta Features' do
|
200
|
+
action :beta_chat, 'Beta Chat App'
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
To associate different views to the same action:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
Consent.define Project, 'Projects' do
|
208
|
+
# returns conditions that can be used as a matcher for objects so the matcher
|
209
|
+
# can return true or false (hash version)
|
210
|
+
view :department, "User's department only" do |user|
|
211
|
+
{ department_id: user.id }
|
212
|
+
end
|
213
|
+
view :future_projects, "User's department only",
|
214
|
+
# returns a condition to be applied to a collection of objects
|
215
|
+
-> (_) {
|
216
|
+
['starts_at > ?', Date.today]
|
217
|
+
end,
|
218
|
+
# returns true/false based on a condition -- to use this, you must pass in
|
219
|
+
# an instance of an object in order to check the permission
|
220
|
+
-> (user, project) {
|
221
|
+
project.starts_at > Date.today
|
222
|
+
}
|
223
|
+
|
224
|
+
action :read, 'Read projects', views: [:department, :future_projects]
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
If you have a set of actions with the same set of views, you can use a
|
229
|
+
`with_defaults` block to simplify the writing:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
with_defaults views: [:department, :small_volumes] do
|
233
|
+
action :read, 'Read projects'
|
234
|
+
action :approve, 'Approve projects'
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
### Permission
|
239
|
+
|
240
|
+
Permission is what is granted to a role, or a user. It grants the ability to perform an *action*,
|
241
|
+
on a limited scope (*view*) of the *subject*.
|
242
|
+
|
243
|
+
## CanCan Integration
|
244
|
+
|
245
|
+
Consent provides a [CanCan](https://github.com/CanCanCommunity/cancancan#readme) ability (Consent::Ability)
|
246
|
+
that can be initialized with a group of granted permissions. You can initialize a `Consent::Ability` with:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
Consent::Ability.new(*context, super_user: <true|false>, apply_defaults: <true|false>, permissions: [Consent::Permission, ...])
|
250
|
+
```
|
251
|
+
|
252
|
+
- `*context` is what is given to the view evaluating permission rules. That is typically a user;
|
253
|
+
- `super_user` makes the ability to respond to `true` to any `can?` questions, and yields no
|
254
|
+
restrictions in any `accessible_by` and `accessible_through` queries;
|
255
|
+
- `apply_defaults` grants actions with the `default_view` set automatically.
|
256
|
+
- `permissions` is a collection of permissions to grant to the user
|
257
|
+
|
258
|
+
### Manually consent permissions
|
259
|
+
|
260
|
+
You can manually grant permissions with `consent`. You could possibly subclass
|
261
|
+
`Consent::Ability` to consent some specific permissions by default:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class MyAbility < Consent::Ability
|
265
|
+
def initialize(...)
|
266
|
+
super(...)
|
267
|
+
|
268
|
+
consent action: :read, subject: Project, view: :department
|
269
|
+
end
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
You can also consent full-access by not specifying the view:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
consent action: :read, subject: Project
|
277
|
+
```
|
278
|
+
|
279
|
+
Consenting the same permission multiple times is handled as a Union by CanCanCan:
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
class MyAbility < Consent::Ability
|
283
|
+
def initialize(user)
|
284
|
+
super user
|
285
|
+
|
286
|
+
consent :read, Project, :department
|
287
|
+
consent :read, Project, :future_projects
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
user = User.new(department_id: 13)
|
292
|
+
ability = MyAbility.new(user)
|
293
|
+
|
294
|
+
Project.accessible_by(ability, :read).to_sql
|
295
|
+
=> SELECT * FROM projects WHERE ((department_id = 13) OR (starts_at > '2021-04-06'))
|
296
|
+
```
|
297
|
+
|
298
|
+
## Special subject and actions
|
299
|
+
|
300
|
+
### :manage
|
301
|
+
|
302
|
+
Whenever the `:manage` action is granted to a user through `consent` or `can`, that means that all actions in that subject are automatically granted with the same restrictions. Take the following example:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
Consent.define User, "User permissions" do
|
306
|
+
view :all, "All users"
|
307
|
+
view :self, "Own user" do |user|
|
308
|
+
{ id: user.id }
|
309
|
+
end
|
310
|
+
|
311
|
+
action :manage, "Manage users", views: %i[self all]
|
312
|
+
action :update, "Manage users"
|
313
|
+
end
|
314
|
+
|
315
|
+
> ability = Consent::Ability.new(User.new(id: 123))
|
316
|
+
> ability.consent subject: User, action: :manage, view: :self
|
317
|
+
> User.accessible_by(ability, :manage).to_sql
|
318
|
+
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 123"
|
319
|
+
> User.accessible_by(ability, :view).to_sql
|
320
|
+
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 123"
|
321
|
+
```
|
322
|
+
|
323
|
+
### :all
|
324
|
+
|
325
|
+
In CanCan, `:all` is a special subject which means `all subjects`. Whatever is granted to `:all` is applied to all subjects. I.e.:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
> ability = Consent::Ability.new(User.new(id: 123))
|
329
|
+
> User.accessible_by(ability, :update).to_sql
|
330
|
+
=> "SELECT `users`.* FROM `users` WHERE 1=0"
|
331
|
+
> ability.can :update, :all
|
332
|
+
> User.accessible_by(ability, :update).to_sql
|
333
|
+
=> "SELECT `users`.* FROM `users`"
|
334
|
+
```
|
335
|
+
|
336
|
+
## Rails Integration
|
337
|
+
|
338
|
+
Consent is integrated into Rails with `Consent::Engine`. To define where
|
339
|
+
your permission files will be, use `config.consent.path`. This defaults to
|
340
|
+
`#{Rails.root}/app/permissions/` to conform to Rails' standards.
|
341
|
+
|
342
|
+
## Development
|
343
|
+
|
344
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
345
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
346
|
+
prompt that will allow you to experiment.
|
347
|
+
|
348
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
349
|
+
release a new version, update the version number in `version.rb`, and then run
|
350
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
351
|
+
git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
352
|
+
|
353
|
+
## Contributing
|
354
|
+
|
355
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/powerhome/consent.
|
data/lib/consent/ability.rb
CHANGED
@@ -1,15 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Consent
|
4
|
-
#
|
4
|
+
#
|
5
|
+
# Defines a CanCan(Can)::Ability class based on Consent::Permissions
|
6
|
+
#
|
5
7
|
class Ability
|
6
8
|
include CanCan::Ability
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
#
|
11
|
+
# Initialize a Consent::Ability consenting all the given permissions.
|
12
|
+
#
|
13
|
+
# When super_user is set to true, it grants `:manage :all`, which is understood by
|
14
|
+
# CanCan as a keyword to allow everything with no restrictions.
|
15
|
+
#
|
16
|
+
# If apply_defaults is set to true, Consent::Ability will grant the default views
|
17
|
+
# defined in the permissions.
|
18
|
+
#
|
19
|
+
# I.e.:
|
20
|
+
#
|
21
|
+
# Consent.define Project, 'Projects' do
|
22
|
+
# view :department, "User's department only" do |user|
|
23
|
+
# { department_id: user.id }
|
24
|
+
# end
|
25
|
+
# view :self, "User's own projects" do |user|
|
26
|
+
# { user_id: user.id }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# action :close, views: %i[department self], default_view: :self
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Consent::Ability.new(user, permissions: user&.permissions,
|
33
|
+
# super_user: user&.super_user?,
|
34
|
+
# apply_defaults: user.present?)
|
35
|
+
#
|
36
|
+
# @param [*] *context the view context, usually the user and some additional information
|
37
|
+
# @param [Array<Consent::Permission>] permissions the list of permissions to grant
|
38
|
+
# @param [Boolean] super_user whether Consent should grant :manage :all
|
39
|
+
# @param [Boolean] apply_defaults whether Consent should grant default views
|
40
|
+
#
|
41
|
+
def initialize(*context, permissions: nil, super_user: false, apply_defaults: true)
|
42
|
+
@context = *context
|
43
|
+
|
10
44
|
apply_defaults! if apply_defaults
|
45
|
+
can :manage, :all if super_user
|
46
|
+
|
47
|
+
permissions&.each do |permission|
|
48
|
+
consent(**permission.slice(:subject, :action, :view).symbolize_keys)
|
49
|
+
end
|
11
50
|
end
|
12
51
|
|
52
|
+
# Consents a subject/action/view to the ability
|
53
|
+
#
|
54
|
+
# `consent!` will add a `can` permission to the ability based on the
|
55
|
+
# view rules defined in the Consent definitions.
|
56
|
+
#
|
57
|
+
# @param [Class,Symbol] subject the target subject of the action
|
58
|
+
# @param [Symbol] action the action being granted on the subject
|
59
|
+
# @param [Symbol,nil] view the conditions/rules on which the action is granted
|
60
|
+
# @raises Consent::ViewNotFound when the view key doesn't exist in the context
|
61
|
+
#
|
13
62
|
def consent!(subject: nil, action: nil, view: nil)
|
14
63
|
view = case view
|
15
64
|
when Consent::View
|
@@ -24,13 +73,55 @@ module Consent
|
|
24
73
|
)
|
25
74
|
end
|
26
75
|
|
76
|
+
# Consents a subject/action/view to the ability
|
77
|
+
#
|
78
|
+
# @see ::Consent::Ability#consent
|
79
|
+
#
|
27
80
|
def consent(**kwargs)
|
28
81
|
consent!(**kwargs)
|
29
82
|
rescue Consent::ViewNotFound
|
30
83
|
nil
|
31
84
|
end
|
32
85
|
|
33
|
-
|
86
|
+
# Returns a hash where the keys are the given permissions, and the values
|
87
|
+
# are either true or false, representing their ability to perform the given
|
88
|
+
# permision
|
89
|
+
#
|
90
|
+
# @param [Array<String>,String,nil] permissions an array of the requested permissions
|
91
|
+
# @return [Hash<String,Boolean>] the hash with the results
|
92
|
+
def to_h(permissions = nil)
|
93
|
+
Array(permissions).reduce({}) do |result, permission|
|
94
|
+
result.merge permission => can?(permission)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check if the user has permission to perform a given action on an object.
|
99
|
+
#
|
100
|
+
# can? :destroy, @project
|
101
|
+
#
|
102
|
+
# You can also pass the class instead of an instance (if you don't have one handy).
|
103
|
+
#
|
104
|
+
# can? :create, Project
|
105
|
+
#
|
106
|
+
# You can also check with string form of the permission:
|
107
|
+
#
|
108
|
+
# can? "project/create"
|
109
|
+
#
|
110
|
+
# For more info, check the documentation of [CanCan::Ability]
|
111
|
+
def can?(action_or_pair, subject = nil, *args)
|
112
|
+
action, subject = extract_action_subject(action_or_pair, subject)
|
113
|
+
super action, subject, *args
|
114
|
+
end
|
115
|
+
|
116
|
+
# @private
|
117
|
+
def relation_model_adapter(model_class, action_or_pair, subject, relation)
|
118
|
+
action, subject = extract_action_subject(action_or_pair, subject)
|
119
|
+
::CanCan::ModelAdapters::AbstractAdapter
|
120
|
+
.adapter_class(model_class)
|
121
|
+
.new(model_class, relation_rules(model_class, action, subject, relation))
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
34
125
|
|
35
126
|
def apply_defaults!
|
36
127
|
Consent.subjects.each do |subject|
|
@@ -45,5 +136,23 @@ module Consent
|
|
45
136
|
end
|
46
137
|
end
|
47
138
|
end
|
139
|
+
|
140
|
+
def relation_rules(model_class, action, subject, relation)
|
141
|
+
relevant_rules(action, subject).map do |rule|
|
142
|
+
unless rule.conditions.is_a?(Hash)
|
143
|
+
raise ::CanCan::Error, "accessible_through is only available with hash conditions"
|
144
|
+
end
|
145
|
+
|
146
|
+
conditions = rule.conditions.dig(*Array(relation))
|
147
|
+
::CanCan::Rule.new(rule.base_behavior, action, model_class, conditions, rule.block)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def extract_action_subject(action_or_string_pair, subject)
|
152
|
+
return action_or_string_pair, subject if subject
|
153
|
+
|
154
|
+
subject_key, _, action_key = action_or_string_pair.rpartition("/")
|
155
|
+
[action_key.to_sym, Consent::SubjectCoder.load(subject_key)]
|
156
|
+
end
|
48
157
|
end
|
49
158
|
end
|
data/lib/consent/dsl.rb
CHANGED
@@ -1,26 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "consent"
|
4
4
|
|
5
5
|
module Consent
|
6
6
|
# Plugs consent permission load to the Rails class loading cycle
|
7
|
-
class
|
7
|
+
class Engine < Rails::Engine
|
8
8
|
config.before_configuration do |app|
|
9
|
-
default_path = app.root.join(
|
10
|
-
config.consent = Consent::Reloader.new(
|
11
|
-
default_path,
|
12
|
-
ActiveSupport::Dependencies.mechanism
|
13
|
-
)
|
9
|
+
default_path = app.root.join("app", "permissions")
|
10
|
+
config.consent = Consent::Reloader.new(default_path)
|
14
11
|
end
|
15
12
|
|
16
13
|
config.after_initialize do |app|
|
17
14
|
app.config.consent.execute
|
18
15
|
end
|
19
16
|
|
20
|
-
initializer
|
17
|
+
initializer "consent.reloader" do |app|
|
21
18
|
app.reloaders << config.consent
|
22
19
|
ActiveSupport::Dependencies.autoload_paths -= config.consent.paths
|
23
20
|
config.to_prepare { app.config.consent.execute }
|
24
21
|
end
|
22
|
+
|
23
|
+
initializer "consent.accessible_through" do
|
24
|
+
ActiveSupport.on_load(:active_record) do
|
25
|
+
include Consent::ModelAdditions
|
26
|
+
end
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consent
|
4
|
+
#
|
5
|
+
# Accessible Through logic
|
6
|
+
#
|
7
|
+
# This module adds the accessible_through class method to a model.
|
8
|
+
# It is included in the activerecord base classes by Consent::Engine.
|
9
|
+
#
|
10
|
+
# @see Consent::ModelAdditions::ClassMethods#accessible_through
|
11
|
+
#
|
12
|
+
module ModelAdditions
|
13
|
+
module ClassMethods
|
14
|
+
# Provides a scope within the model to find instances of the model that are accessible
|
15
|
+
# by the given ability through a given relation in the main subject
|
16
|
+
#
|
17
|
+
# I.E.:
|
18
|
+
#
|
19
|
+
# Given the following scenario
|
20
|
+
#
|
21
|
+
# class User
|
22
|
+
# belongs_to :territory
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Consent.define User, "User permissions" do
|
26
|
+
# view :territory do |user|
|
27
|
+
# { territory: { id: user.territory_id } }
|
28
|
+
# end
|
29
|
+
# view :visible_territories do |user|
|
30
|
+
# { territory: { id: user.visible_territory_ids } }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# action :contact, views: %i[all no_access territory visible_territories]
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# This would give you a list of territories that the given ability can
|
37
|
+
# contact their users:
|
38
|
+
#
|
39
|
+
# > user = User.new(territory_id: 13, visible_territory_ids: [2, 3, 4])
|
40
|
+
# > ability = Consent::Ability.new(user.to_session_user)
|
41
|
+
# > ability.consent view: :territory, action: :contact, subject: User
|
42
|
+
# > Territory.accessible_through(ability, :contact, User).to_sql
|
43
|
+
# => SELECT * FROM territories WHERE id = 13
|
44
|
+
# > ability.consent view: :visible_territories, action: :contact, subject: User
|
45
|
+
# > Territory.accessible_through(ability, :contact, User).to_sql
|
46
|
+
# => SELECT * FROM territories WHERE ((id = 13) OR (id IN (2, 3, 4)))
|
47
|
+
#
|
48
|
+
# @param ability [Consent::Ability] ability performing the query
|
49
|
+
# @param action_or_pair [Symbol,String] the name of the action or a subject/action pair
|
50
|
+
# @param subject [Class,Symbol,nil] the subject in which the action is, when action_or_pair is just the action
|
51
|
+
# @param relation [Symbol,Array<Symbol>] the relation or path to the relation
|
52
|
+
#
|
53
|
+
def accessible_through(ability, action_or_pair, subject = nil, relation: nil)
|
54
|
+
relation ||= model_name.element.to_sym
|
55
|
+
ability.relation_model_adapter(self, action_or_pair, subject, relation)
|
56
|
+
.database_records
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.included(base)
|
61
|
+
base.extend ClassMethods
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|