consent 1.0.1 → 2.1.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -6
  3. data/.rubocop.yml +17 -0
  4. data/.rubocop_todo.yml +9 -11
  5. data/Appraisals +13 -0
  6. data/Gemfile +10 -1
  7. data/Gemfile.lock +259 -0
  8. data/Rakefile +9 -3
  9. data/app/models/concerns/consent/authorizable.rb +94 -0
  10. data/app/models/consent/application_record.rb +7 -0
  11. data/app/models/consent/history.rb +21 -0
  12. data/app/models/consent/permission.rb +71 -0
  13. data/bin/console +3 -3
  14. data/config.ru +9 -0
  15. data/consent.gemspec +25 -21
  16. data/db/migrate/20211104225614_create_nitro_auth_authorization_permissions.rb +19 -0
  17. data/db/migrate/20220420135558_create_nitro_auth_authorization_histories.rb +15 -0
  18. data/doc/dependency_decisions.yml +3 -0
  19. data/docs/CHANGELOG.md +32 -0
  20. data/docs/README.md +355 -0
  21. data/gemfiles/.bundle/config +2 -0
  22. data/gemfiles/rails_6_1.gemfile +15 -0
  23. data/gemfiles/rails_6_1.gemfile.lock +287 -0
  24. data/gemfiles/rails_7_0.gemfile +15 -0
  25. data/gemfiles/rails_7_0.gemfile.lock +286 -0
  26. data/gemfiles/rails_7_1.gemfile +15 -0
  27. data/gemfiles/rails_7_1.gemfile.lock +337 -0
  28. data/lib/consent/ability.rb +113 -4
  29. data/lib/consent/dsl.rb +1 -8
  30. data/lib/consent/{railtie.rb → engine.rb} +11 -8
  31. data/lib/consent/model_additions.rb +64 -0
  32. data/lib/consent/permission_migration.rb +139 -0
  33. data/lib/consent/reloader.rb +6 -5
  34. data/lib/consent/rspec/consent_action.rb +7 -7
  35. data/lib/consent/rspec/consent_view.rb +10 -14
  36. data/lib/consent/rspec.rb +3 -3
  37. data/lib/consent/subject_coder.rb +39 -0
  38. data/lib/consent/symbol_adapter.rb +18 -0
  39. data/lib/consent/version.rb +1 -1
  40. data/lib/consent.rb +25 -13
  41. data/lib/generators/consent/permissions_generator.rb +5 -5
  42. data/mkdocs.yml +6 -0
  43. data/renovate.json +15 -2
  44. metadata +126 -37
  45. data/.rspec +0 -2
  46. data/.ruby-version +0 -1
  47. data/.travis.yml +0 -20
  48. data/LICENSE +0 -21
  49. data/README.md +0 -252
  50. 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
@@ -0,0 +1,3 @@
1
+ ---
2
+ - - :inherit_from
3
+ - https://raw.githubusercontent.com/powerhome/oss-guide/master/license_rules.yml
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 [![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.
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -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: "../"