consent 1.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/powerhome/consent.svg?branch=master)](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
|