consent 1.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +13 -6
- data/.rubocop.yml +17 -0
- data/.rubocop_todo.yml +9 -11
- data/Appraisals +13 -0
- data/Gemfile +10 -1
- data/Gemfile.lock +259 -0
- 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 +21 -0
- data/app/models/consent/permission.rb +71 -0
- data/bin/console +3 -3
- data/config.ru +9 -0
- data/consent.gemspec +25 -21
- 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 +32 -0
- data/docs/README.md +355 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_6_1.gemfile +15 -0
- data/gemfiles/rails_6_1.gemfile.lock +287 -0
- data/gemfiles/rails_7_0.gemfile +15 -0
- data/gemfiles/rails_7_0.gemfile.lock +286 -0
- data/gemfiles/rails_7_1.gemfile +15 -0
- data/gemfiles/rails_7_1.gemfile.lock +337 -0
- data/lib/consent/ability.rb +113 -4
- data/lib/consent/dsl.rb +1 -8
- 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 +10 -14
- data/lib/consent/rspec.rb +3 -3
- data/lib/consent/subject_coder.rb +39 -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 +6 -0
- data/renovate.json +15 -2
- metadata +126 -37
- 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
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateNitroAuthAuthorizationPermissions < ActiveRecord::Migration[5.2]
|
4
|
+
def change
|
5
|
+
create_table :"#{Consent.table_name_prefix}permissions" do |t|
|
6
|
+
t.string :subject, limit: 80
|
7
|
+
t.string :action, limit: 80
|
8
|
+
t.string :view, limit: 80
|
9
|
+
t.integer :role_id
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :"#{Consent.table_name_prefix}permissions", :role_id
|
15
|
+
add_index :"#{Consent.table_name_prefix}permissions", :subject
|
16
|
+
add_index :"#{Consent.table_name_prefix}permissions", %i[subject action],
|
17
|
+
name: :"idx_#{Consent.table_name_prefix}permissions_on_subject_and_action"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateNitroAuthAuthorizationHistories < ActiveRecord::Migration[5.2]
|
4
|
+
def change
|
5
|
+
create_table :"#{Consent.table_name_prefix}histories" do |t|
|
6
|
+
t.string :command, limit: 6
|
7
|
+
t.string :subject, limit: 80
|
8
|
+
t.string :action, limit: 80
|
9
|
+
t.string :view, limit: 80
|
10
|
+
t.integer :role_id
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/docs/CHANGELOG.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [2.1.0] - 2025-02-19
|
4
|
+
|
5
|
+
- Add support to rails 7.1+ [#308](https://github.com/powerhome/power-tools/pull/308)
|
6
|
+
- removed eval_view as a legacy, unsafe concern
|
7
|
+
|
8
|
+
## [2.0.1] - 2023-01-08
|
9
|
+
|
10
|
+
- Bugfixes, minor version bumps
|
11
|
+
|
12
|
+
## [2.0.0] - 2022-09-02
|
13
|
+
|
14
|
+
- Support multiple versions of rails (>= 5.2.8.1)
|
15
|
+
- Consent is now an engine, and provides the ability to store and manage permissions for a Consent::Authorizable class (i.e.: Role)
|
16
|
+
- Consent::Ability is improved to support the new Permission model
|
17
|
+
|
18
|
+
## [1.0.1] - 2021-05-10
|
19
|
+
|
20
|
+
- Improve Rspec matchers
|
21
|
+
|
22
|
+
## [1.0.0] - 2021-04-22
|
23
|
+
|
24
|
+
- Consent released with DSL to design permissions and convenient CanCan::Ability implementation.
|
25
|
+
|
26
|
+
## [0.6.0] - 2020-12-22
|
27
|
+
## [0.5.2] - 2019-11-28
|
28
|
+
## [0.5.0] - 2019-11-25
|
29
|
+
## [0.4.3] - 2019-04-02
|
30
|
+
## [0.4.2] - 2019-03-28
|
31
|
+
## [0.4] - 2019-03-27
|
32
|
+
## [0.3.1] - 2016-11-08
|
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.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "base64"
|
6
|
+
gem "bigdecimal"
|
7
|
+
gem "logger"
|
8
|
+
gem "mutex_m"
|
9
|
+
gem "net-imap", "< 0.5.0"
|
10
|
+
gem "nokogiri", "< 1.18"
|
11
|
+
gem "rubocop-powerhome", path: "../../rubocop-powerhome"
|
12
|
+
gem "zeitwerk", "< 2.7.0"
|
13
|
+
gem "rails", "6.1.7.7"
|
14
|
+
|
15
|
+
gemspec path: "../"
|