consent 0.4.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cfaa01c1cf6ece41d10032df322ad2441d19ff78888b8f6821e2d149a7ee2bd
4
- data.tar.gz: 343ac5abc8e4a49bdd04f4ec2ea998ab6408fb83cd0a9b2a54ddb750bc3c494d
3
+ metadata.gz: 66e7e1705d61760713253718eabda896ffa49f25ea6e1a9a6c4fc1a188730cfb
4
+ data.tar.gz: 4dc120c6f1e89a3cb612a62cf9fdbac565e51b9fabe9d04a68c64c3d9a8fd193
5
5
  SHA512:
6
- metadata.gz: 4723a478ad632b41223aeb185dc681edf454fc7879a22c7c7267ab2e4ad53e02a44d71c1d73732c792e7c25a921545a35669695a6536f8317d430c0066121f77
7
- data.tar.gz: 6bebde1fc871b955daeb0af93547221a91551f8bda579f8986257663fdcee5175d1b318225adb1483c0e3852485185bb85d86a9ed6c2f71378c3a3e8dcc10459
6
+ metadata.gz: 9ca74d2788b689711966b38e11ce4b857009064d1fce59c2b5335adc437c1b20a9298acb61a0e71cf3aac2eef1f13f6b8b06570014938ef757374b6a4f98b9c4
7
+ data.tar.gz: 9d4e6cf5d18d0671c9aade10a33933cc93b0cb704c623d6705332de55a1a02b3952b01fdee9d357a1c0ba9f37d570910ac1fe54614087cd88c94d3d890b5761d
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /consent-*.gem
data/.rubocop.yml ADDED
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,21 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2019-11-20 02:06:29 -0300 using RuboCop version 0.65.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 5
10
+ # Configuration parameters: CountComments, ExcludedMethods.
11
+ # ExcludedMethods: refine
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - 'spec/**/*'
15
+ - 'lib/consent/rspec.rb'
16
+
17
+ # Offense count: 9
18
+ Style/Documentation:
19
+ Exclude:
20
+ - 'spec/**/*'
21
+ - 'test/**/*'
data/.travis.yml CHANGED
@@ -1,9 +1,14 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.2.2
5
- - 2.5.0
4
+ - 2.5.8
5
+ - 2.6.6
6
+ - 2.7.2
7
+ - 3.0.0
6
8
  before_install: gem install bundler -v 1.17.3
9
+ script:
10
+ - bundle exec rubocop
11
+ - bundle exec rspec
7
12
  deploy:
8
13
  provider: rubygems
9
14
  api_key:
@@ -12,3 +17,4 @@ deploy:
12
17
  on:
13
18
  tags: true
14
19
  repo: powerhome/consent
20
+ ruby: 2.6.6
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in consent.gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright Power Home Remodeling Group, LLC
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -18,21 +18,21 @@ Or install it yourself as:
18
18
 
19
19
  ## What is Consent
20
20
 
21
- Consent makes defining permissions easier by providing a clean, concise DSL for authorization so that all abilities do not have to be in your `Ability`
21
+ Consent makes defining permissions easier by providing a clean, concise DSL for authorization
22
+ so that all abilities do not have to be in your `Ability`
22
23
  class.
23
24
 
24
- Consent takes application permissions and models them so that permissions are organized and can be defined granularly. It does so using the
25
- following models:
25
+ Consent takes application permissions and models them so that permissions are organized and can
26
+ be defined granularly. It does so using the following models:
26
27
 
27
28
  * View: A collection of objects limited by a given condition.
28
29
  * Action: An action performed on top of the objects limited by the view. For example, one user could only `:view` something, while another could `:manage` it.
29
30
  * Subject: Holds the scope of the actions.
30
- * Permission: What is given to the user. Combines a subject, an action and
31
- a view.
31
+ * Permission: The combination of a subject, an action, and a view (or full-access).
32
32
 
33
33
  ## What Consent Is Not
34
34
 
35
- Consent isn't a tool to enforce permissions -- it is intended to be used with CanCanCan and is only to make permissions more easily readable and definable.
35
+ Consent isn't a tool to enforce permissions -- it supports CanCan(Can) for that goal.
36
36
 
37
37
  ## Subject
38
38
 
@@ -50,7 +50,8 @@ Consent.define Project, 'Our Projects' do
50
50
  end
51
51
  ```
52
52
 
53
- The scope is the action that's being performed on the subject. It can be anything, but will typically be an ActiveRecord class, a `:symbol`, or a PORO.
53
+ The scope is the action that's being performed on the subject. It can be anything, but will
54
+ typically be an ActiveRecord class, a `:symbol`, or a PORO.
54
55
 
55
56
  For instance:
56
57
 
@@ -62,16 +63,15 @@ end
62
63
 
63
64
  ## Views
64
65
 
65
- Views are the rules that limit the access to actions. For instance,
66
- a user may see a `Project` from his department, but not from others. That rule
67
- could be enforced with a `:department` view, defined like this:
66
+ Views are the rules that limit access to actions. For instance, a user may see a `Project`
67
+ from his department, but not from others. You can enforce it with a `:department` view,
68
+ as in the examples below:
68
69
 
69
70
  ### Hash Conditions
70
71
 
71
- This is probably the most commonly used and is useful, for example,
72
- when the view can be defined using a where condition in an ActiveRecord context.
73
- It follows a match condition and will return all objects that meet the criteria
74
- and is based off a boolean:
72
+ Probably the most commonly used. When the view can be defined using a `where` scope in
73
+ an ActiveRecord context. It follows a match condition and will return all objects that meet
74
+ the criteria:
75
75
 
76
76
  ```ruby
77
77
  Consent.define Project, 'Projects' do
@@ -81,22 +81,21 @@ Consent.define Project, 'Projects' do
81
81
  end
82
82
  ```
83
83
 
84
- Although hash conditions (matching object's attributes) are recommended,
85
- the constraints can be anything you want. Since Consent does not enforce the
86
- rules, those rules are directly given to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice)
84
+ Although hash conditions (matching object's attributes) are recommended, the constraints can
85
+ be anything you want. Since Consent does not enforce the rules, those rules are directly given
86
+ to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice)
87
87
  for defining abilities is recommended.
88
88
 
89
89
  ### Object Conditions
90
90
 
91
- If you're not matching for equal values, then you would need to use an object
92
- condition, which matches data based off a range.
91
+ If you're not matching for equal values, then you would need to use an object condition.
93
92
 
94
- If you already have an object and want to check to see whether the user has
95
- permission to view that specific object, you would use object conditions.
93
+ If you already have an object and want to check to see whether the user has permission to view
94
+ that specific object, you would use object conditions.
96
95
 
97
- If your needs can't be satisfied by hash conditions, it is recommended that a
98
- second condition is given for constraining object instances. For example, if you
99
- want to restrict a view for smaller volume projects:
96
+ If your needs can't be satisfied by hash conditions, it is recommended that a second condition
97
+ is given for constraining object instances. For example, if you want to restrict a view for smaller
98
+ volume projects:
100
99
 
101
100
  ```ruby
102
101
  Consent.define Project, 'Projects' do
@@ -111,7 +110,7 @@ end
111
110
  ```
112
111
 
113
112
  For object conditions, the latter argument will be the referred object, while the
114
- former will be the context given to the [Permission](#permission) (also check
113
+ first will be the context given to the [Permission](#permission) (also check
115
114
  [CanCan integration](#cancan-integration)).
116
115
 
117
116
  ## Action
@@ -161,73 +160,81 @@ end
161
160
 
162
161
  ## Permission
163
162
 
164
- A permission is what is consented to the user. It is the *permission* to perform
163
+ A permission is what is consented to the user. It consentment to perform
165
164
  an *action* on a limited *view* of the *subject*. It marries the three concepts
166
165
  to consent an access to the user.
167
166
 
168
- A permission is not specified by the user, it is calculated from a permissions
169
- hash owned by a `User`, or a `Role` on an application.
167
+ ## CanCan Integration
170
168
 
171
- The permissions hash looks like the following:
169
+ Consent provides a CanCan ability (Consent::Ability) to integrate your
170
+ permissions with frameworks like Rails. To use it with Rails check out the
171
+ example at [Ability for Other Users](https://github.com/CanCanCommunity/cancancan/wiki/Ability-for-Other-Users)
172
+ on CanCanCan's wiki.
173
+
174
+ In the ability you define the scope of the permissions. This is typically a
175
+ user:
172
176
 
173
177
  ```ruby
174
- {
175
- project: {
176
- read: 'department',
177
- approve: 'small_volumes'
178
- }
179
- }
178
+ Consent::Ability.new(user)
180
179
  ```
181
180
 
182
- In other words:
181
+ You'd more commonly define a subclass of `Consent::Ability`, and consent access
182
+ to the user by calling `consent`:
183
183
 
184
184
  ```ruby
185
- {
186
- <subject>: {
187
- <action>: <view>
188
- }
189
- }
185
+ class MyAbility < Consent::Ability
186
+ def initialize(user)
187
+ super user
188
+
189
+ consent :read, Project, :department
190
+ end
191
+ end
190
192
  ```
191
193
 
192
- ### Full Access
194
+ You can also consent full access by not specifying the view:
193
195
 
194
- Full (unrestricted by views) access is granted when view is `'1'`, `true` or
195
- `'true'`. For instance:
196
+ ```ruby
197
+ consent :read, Project
198
+ ```
196
199
 
197
- In other words:
200
+ If you have a somehow manageable permission, you can consent them in batch in your ability:
198
201
 
199
202
  ```ruby
200
- {
201
- projects: {
202
- approve: true
203
- }
204
- }
203
+ class MyAbility < Consent::Ability
204
+ def initialize(user)
205
+ super user
206
+
207
+ user.permissions.each do |permission|
208
+ consent permission.action, permission.subject, permission.view
209
+ end
210
+ end
211
+ end
205
212
  ```
206
213
 
207
- ## CanCan Integration
214
+ Consenting the same permission multiple times is handled as a Union by CanCanCan:
208
215
 
209
- Consent provides a CanCan ability (Consent::Ability) to integrate your
210
- permissions with frameworks like Rails. To use it with Rails check out the
211
- example at [Ability for Other Users](https://github.com/CanCanCommunity/cancancan/wiki/Ability-for-Other-Users)
212
- on CanCanCan's wiki.
216
+ ```ruby
217
+ class MyAbility < Consent::Ability
218
+ def initialize(user)
219
+ super user
213
220
 
214
- In the ability you define the scope of the permissions. This is typically an
215
- user:
221
+ consent :read, Project, :department
222
+ consent :read, Project, :future_projects
223
+ end
224
+ end
216
225
 
217
- ```ruby
218
- Consent::Ability.new(user.permissions, user)
219
- ```
226
+ user = User.new(department_id: 13)
227
+ ability = MyAbility.new(user)
220
228
 
221
- The first parameter given to the ability is the permissions hash, seen at
222
- [Permission](#permission). The following parameters are the permission context.
223
- These parameters are given directly to the condition blocks defined by the views
224
- in the exact same order, so it's up to you to define what your context is.
229
+ Project.accessible_by(ability, :read).to_sql
230
+ => SELECT * FROM projects WHERE ((department_id = 13) OR (starts_at > '2021-04-06'))
231
+ ```
225
232
 
226
233
  ## Rails Integration
227
234
 
228
235
  Consent is integrated into Rails with `Consent::Railtie`. To define where
229
236
  your permission files will be, use `config.consent.path`. This defaults to
230
- `app/permissions/` to conform to Rails' standards.
237
+ `#{Rails.root}/app/permissions/` to conform to Rails' standards.
231
238
 
232
239
  ## Development
233
240
 
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "consent"
4
+ require 'bundler/setup'
5
+ require 'consent'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "consent"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start
data/consent.gemspec CHANGED
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'consent/version'
5
6
 
@@ -12,14 +13,16 @@ Gem::Specification.new do |spec|
12
13
  spec.summary = 'Consent'
13
14
  spec.description = 'Consent'
14
15
 
16
+ spec.licenses = ['MIT']
17
+
15
18
  spec.files = `git ls-files`.split.reject do |file|
16
19
  file =~ /^(test|spec|features)/
17
20
  end
18
21
  spec.require_paths = ['lib']
19
22
 
20
- spec.add_development_dependency 'activesupport', '>= 4.1.11'
23
+ spec.add_development_dependency 'bundler', '>= 1.17.3'
21
24
  spec.add_development_dependency 'cancancan', '~> 1.15.0'
22
- spec.add_development_dependency 'bundler', '~> 1.17.3'
23
- spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rake', '>= 12.3.3'
24
26
  spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'rubocop', '~> 0.65.0'
25
28
  end
data/lib/consent.rb CHANGED
@@ -1,30 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'consent/version'
2
4
  require 'consent/subject'
3
5
  require 'consent/view'
4
6
  require 'consent/action'
5
7
  require 'consent/dsl'
6
8
  require 'consent/permission'
7
- require 'consent/permissions'
8
9
  require 'consent/ability' if defined?(CanCan)
9
10
  require 'consent/railtie' if defined?(Rails)
10
11
 
12
+ # Consent makes defining permissions easier by providing a clean,
13
+ # concise DSL for authorization so that all abilities do not have
14
+ # to be in your `Ability` class.
11
15
  module Consent
12
- FULL_ACCESS = %w(1 true).freeze
13
-
16
+ # Default views available to every permission
17
+ #
18
+ # i.e.:
19
+ # Defining a view with no conditions:
20
+ # Consent.default_views[:all] = Consent::View.new(:all, "All")
21
+ #
22
+ # @return [Hash<Symbol,Consent::View>]
14
23
  def self.default_views
15
24
  @default_views ||= {}
16
25
  end
17
26
 
27
+ # Subjects defined in Consent
28
+ #
29
+ # @return [Array<Consent::Subject>]
18
30
  def self.subjects
19
31
  @subjects ||= []
20
32
  end
21
33
 
34
+ # Finds all subjects defined with the given key
35
+ #
36
+ # @return [Array<Consent::Subject>]
22
37
  def self.find_subjects(subject_key)
23
38
  @subjects.find_all do |subject|
24
39
  subject.key.eql?(subject_key)
25
40
  end
26
41
  end
27
42
 
43
+ # Finds an action within a subject context
44
+ #
45
+ # @return [Consent::Action,nil]
28
46
  def self.find_action(subject_key, action_key)
29
47
  Consent.find_subjects(subject_key)
30
48
  .map(&:actions).flatten
@@ -33,18 +51,36 @@ module Consent
33
51
  end
34
52
  end
35
53
 
54
+ # Finds a view within a subject context
55
+ #
56
+ # @return [Consent::View,nil]
36
57
  def self.find_view(subject_key, view_key)
37
58
  views = Consent.find_subjects(subject_key)
38
- .map{|subject| subject.views}
59
+ .map(&:views)
39
60
  .reduce({}, &:merge)
40
61
  views[view_key]
41
62
  end
42
63
 
43
- def self.load_subjects!(paths)
44
- permission_files = paths.map { |dir| dir.join('*.rb') }
45
- Dir[*permission_files].each(&Kernel.method(:load))
64
+ # Loads all permission (ruby) files from the given directory
65
+ # and using the given mechanism (default: :require)
66
+ #
67
+ # @param paths [Array<String,#to_s>] paths where the ruby files are located
68
+ # @param mechanism [:require,:load] mechanism to load the files
69
+ def self.load_subjects!(paths, mechanism = :require)
70
+ permission_files = paths.map { |dir| File.join(dir, '*.rb') }
71
+ Dir[*permission_files].each(&Kernel.method(mechanism))
46
72
  end
47
73
 
74
+ # Defines a subject with the given key, label and options
75
+ #
76
+ # i.e:
77
+ # Consent.define :users, "User management" do
78
+ # view :department, "Same department only" do |user|
79
+ # { department_id: user.department_id }
80
+ # end
81
+ # action :read, "Can view users"
82
+ # action :update, "Can edit existing user", views: :department
83
+ # end
48
84
  def self.define(key, label, options = {}, &block)
49
85
  defaults = options.fetch(:defaults, {})
50
86
  subjects << Subject.new(key, label).tap do |subject|
@@ -52,6 +88,9 @@ module Consent
52
88
  end
53
89
  end
54
90
 
91
+ # Maps a permissions hash to a Consent::Permissions
92
+ #
93
+ # @return [Consent::Permissions]
55
94
  def self.permissions(permissions)
56
95
  Permissions.new(permissions)
57
96
  end
@@ -1,12 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
4
+ # Defines a CanCan(Can)::Ability class based on a permissions hash
2
5
  class Ability
3
6
  include CanCan::Ability
4
7
 
5
- def initialize(permissions, *args)
6
- Consent.permissions(permissions).each do |permission|
7
- conditions = permission.conditions(*args)
8
- object_conditions = permission.object_conditions(*args)
9
- can permission.action_key, permission.subject_key, conditions, &object_conditions
8
+ def initialize(*args, apply_defaults: true)
9
+ @context = *args
10
+ apply_defaults! if apply_defaults
11
+ end
12
+
13
+ def consent(permission: nil, subject: nil, action: nil, view: nil)
14
+ permission ||= Permission.new(subject, action, view)
15
+ return unless permission.valid?
16
+
17
+ can(
18
+ permission.action_key, permission.subject_key,
19
+ permission.conditions(*@context),
20
+ &permission.object_conditions(*@context)
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def apply_defaults!
27
+ Consent.subjects.each do |subject|
28
+ subject.actions.each do |action|
29
+ next unless action.default_view
30
+
31
+ consent(
32
+ subject: subject.key,
33
+ action: action.key,
34
+ view: action.default_view
35
+ )
36
+ end
10
37
  end
11
38
  end
12
39
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class Action
4
+ class Action # :nodoc:
3
5
  attr_reader :key, :label, :options
4
6
 
5
7
  def initialize(key, label, options = {})
data/lib/consent/dsl.rb CHANGED
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class DSL
4
+ class DSL # :nodoc:
3
5
  attr_reader :subject
4
6
 
5
7
  def initialize(subject, defaults)
@@ -11,13 +13,13 @@ module Consent
11
13
  DSL.build(@subject, @defaults.merge(new_defaults), &block)
12
14
  end
13
15
 
14
- # rubocop:disable Link/UnusedBlockArgument, Link/Eval
16
+ # rubocop:disable Lint/UnusedBlockArgument, Security/Eval
15
17
  def eval_view(key, label, collection_conditions)
16
18
  view key, label do |user|
17
19
  eval(collection_conditions)
18
20
  end
19
21
  end
20
- # rubocop:enable Link/UnusedBlockArgument, Link/Eval
22
+ # rubocop:enable Lint/UnusedBlockArgument, Security/Eval
21
23
 
22
24
  def view(key, label, instance = nil, collection = nil, &block)
23
25
  collection ||= block
@@ -1,29 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class Permission
3
- def initialize(subject, action, view = nil)
4
- @subject = subject
5
- @action = action
6
- @view = view
7
- end
4
+ class Permission # :nodoc:
5
+ attr_reader :subject_key, :action_key, :view_key, :view
8
6
 
9
- def subject_key
10
- @subject.key
7
+ def initialize(subject_key, action_key, view_key = nil)
8
+ @subject_key = subject_key
9
+ @action_key = action_key
10
+ @view_key = view_key
11
+ @view = Consent.find_view(subject_key, view_key) if view_key
11
12
  end
12
13
 
13
- def action_key
14
- @action.key
14
+ def action
15
+ @action ||= Consent.find_action(subject_key, action_key)
15
16
  end
16
17
 
17
- def view_key
18
- @view && @view.key
18
+ def valid?
19
+ action && (@view_key.nil? == @view.nil?)
19
20
  end
20
21
 
21
22
  def conditions(*args)
22
- @view && @view.conditions(*args)
23
+ @view.nil? ? nil : @view.conditions(*args)
23
24
  end
24
25
 
25
26
  def object_conditions(*args)
26
- @view && @view.object_conditions(*args)
27
+ @view.nil? ? nil : @view.object_conditions(*args)
27
28
  end
28
29
  end
29
30
  end
@@ -1,19 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'consent/reloader'
4
+
1
5
  module Consent
2
6
  # Plugs consent permission load to the Rails class loading cycle
3
7
  class Railtie < Rails::Railtie
4
- config.before_configuration do
5
- default_path = Rails.root.join('app', 'permissions')
6
- config.consent = Struct.new(:paths).new([default_path])
8
+ config.before_configuration do |app|
9
+ default_path = app.root.join('app', 'permissions')
10
+ config.consent = Consent::Reloader.new(
11
+ default_path,
12
+ ActiveSupport::Dependencies.mechanism
13
+ )
7
14
  end
8
15
 
9
- config.to_prepare do
10
- Consent.subjects.clear
11
- Consent.load_subjects! Rails.application.config.consent.paths
16
+ config.after_initialize do |app|
17
+ app.config.consent.execute
12
18
  end
13
19
 
14
- config.after_initialize do
15
- permissions_paths = config.consent.paths.map(&:to_s)
16
- ActiveSupport::Dependencies.autoload_paths -= permissions_paths
20
+ initializer 'initialize consent permissions reloading' do |app|
21
+ app.reloaders << config.consent
22
+ ActiveSupport::Dependencies.autoload_paths -= config.consent.paths
23
+ config.to_prepare { app.config.consent.execute }
17
24
  end
18
25
  end
19
26
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consent
4
+ # Rails file reloader to detect permission changes and apply them to consent
5
+ class Reloader
6
+ attr_reader :paths
7
+ delegate :updated?, :execute, :execute_if_updated, to: :updater
8
+
9
+ def initialize(default_path, mechanism)
10
+ @paths = [default_path]
11
+ @mechanism = mechanism
12
+ end
13
+
14
+ private
15
+
16
+ def reload!
17
+ Consent.subjects.clear
18
+ Consent.load_subjects! paths, @mechanism
19
+ end
20
+
21
+ def updater
22
+ @updater ||= ActiveSupport::FileUpdateChecker.new([], globs) { reload! }
23
+ end
24
+
25
+ def globs
26
+ pairs = paths.map { |path| [path.to_s, %w[rb]] }
27
+ Hash[pairs]
28
+ end
29
+ end
30
+ end
data/lib/consent/rspec.rb CHANGED
@@ -1,6 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'consent'
2
4
 
3
5
  module Consent
6
+ # RSpec helpers for consent. Given permissions are loaded,
7
+ # gives you the ability of defining permission specs like
8
+ #
9
+ # Given "users" permissions
10
+ # Consent.define :users, "User management" do
11
+ # view :department, "Same department only" do |user|
12
+ # { department_id: user.department_id }
13
+ # end
14
+ # action :read, "Can view users"
15
+ # action :update, "Can edit existing user", views: :department
16
+ # end
17
+ #
18
+ # RSpec.describe "User permissions" do
19
+ # include Consent::Rspec
20
+ # let(:user) { double(department_id: 15) }
21
+ #
22
+ # it do
23
+ # is_expected.to consent_view(:department, department_id: 15).to(user)
24
+ # end
25
+ # it { is_expected.to consent_action(:read) }
26
+ # it { is_expected.to consent_action(:update).with_views(:department) }
27
+ # end
28
+ #
29
+ # Find more examples at:
30
+ # https://github.com/powerhome/consent
4
31
  module Rspec
5
32
  extend RSpec::Matchers::DSL
6
33
 
@@ -11,17 +38,25 @@ module Consent
11
38
 
12
39
  match do |subject_key|
13
40
  action = Consent.find_action(subject_key, action_key)
14
- action && @views ? action.view_keys.sort.eql?(@views.sort) : !action.nil?
41
+ if action && @views
42
+ values_match?(action.view_keys.sort, @views.sort)
43
+ else
44
+ !action.nil?
45
+ end
15
46
  end
16
47
 
17
48
  failure_message do |subject_key|
18
49
  action = Consent.find_action(subject_key, action_key)
19
- message = "expected %s (%s) to provide action %s" % [
20
- subject_key.to_s, subject.class, action_key
21
- ]
50
+ message = format(
51
+ 'expected %<skey>s (%<sclass>s) to provide action %<action>s',
52
+ skey: subject_key.to_s, sclass: subject.class, action: action_key
53
+ )
22
54
 
23
55
  if action && @views
24
- '%s with views %s, but actual views are %p' % [message, @views, action.view_keys]
56
+ format(
57
+ '%<message>s with views %<views>s, but actual views are %<keys>p',
58
+ message: message, views: @views, keys: action.view_keys
59
+ )
25
60
  else
26
61
  message
27
62
  end
@@ -35,21 +70,36 @@ module Consent
35
70
 
36
71
  match do |subject_key|
37
72
  view = Consent.find_view(subject_key, view_key)
38
- conditions ? view.try(:conditions, *@context).eql?(conditions) : !view.nil?
73
+ if conditions
74
+ view&.conditions(*@context).eql?(conditions)
75
+ else
76
+ !view.nil?
77
+ end
39
78
  end
40
79
 
41
80
  failure_message do |subject_key|
42
81
  view = Consent.find_view(subject_key, view_key)
43
- message = "expected %s (%s) to provide view %s with %p, but" % [
44
- subject_key.to_s, subject.class, view_key, conditions
45
- ]
82
+ message = format(
83
+ 'expected %<skey>s (%<sclass>s) to provide view %<view>s with` \
84
+ `%<conditions>p, but',
85
+ skey: subject_key.to_s, sclass: subject.class,
86
+ view: view_key, conditions: conditions
87
+ )
46
88
 
47
89
  if view && conditions
48
90
  actual_conditions = view.conditions(*@context)
49
- '%s conditions are %p' % [message, actual_conditions]
91
+ format(
92
+ '%<message>s conditions are %<conditions>p',
93
+ message: message, conditions: actual_conditions
94
+ )
50
95
  else
51
- actual_views = Consent.find_subjects(subject_key).map(&:views).map(&:keys).flatten
52
- '%s available views are %p' % [message, actual_views]
96
+ actual_views = Consent.find_subjects(subject_key)
97
+ .map(&:views)
98
+ .map(&:keys).flatten
99
+ format(
100
+ '%<message>s available views are %<views>p',
101
+ message: message, views: actual_views
102
+ )
53
103
  end
54
104
  end
55
105
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class Subject
4
+ class Subject # :nodoc:
3
5
  attr_reader :key, :label, :actions, :views
4
6
 
5
7
  def initialize(key, label)
@@ -8,14 +10,5 @@ module Consent
8
10
  @actions = []
9
11
  @views = Consent.default_views.clone
10
12
  end
11
-
12
- def permission_key
13
- ActiveSupport::Inflector.underscore(@key.to_s).to_sym
14
- end
15
-
16
- def view_for(action, key)
17
- view = @views.keys & action.view_keys & [key]
18
- @views[view.first] || @views[action.default_view]
19
- end
20
13
  end
21
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- VERSION = '0.4.2'.freeze
4
+ VERSION = '1.0.0'
3
5
  end
data/lib/consent/view.rb CHANGED
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class View
4
+ class View # :nodoc:
3
5
  attr_reader :key, :label
4
6
 
5
7
  def initialize(key, label, instance = nil, collection = nil)
@@ -11,11 +13,13 @@ module Consent
11
13
 
12
14
  def conditions(*args)
13
15
  return @collection unless @collection.respond_to?(:call)
16
+
14
17
  @collection.call(*args)
15
18
  end
16
19
 
17
20
  def object_conditions(*args)
18
21
  return @instance unless @instance.respond_to?(:curry)
22
+
19
23
  @instance.curry[*args]
20
24
  end
21
25
  end
@@ -1,14 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Consent
2
- class PermissionsGenerator < Rails::Generators::NamedBase
3
- source_root File.expand_path('../templates', __FILE__)
4
+ class PermissionsGenerator < Rails::Generators::NamedBase # :nodoc:
5
+ source_root File.expand_path('templates', __dir__)
4
6
  argument :description, type: :string, required: false
5
7
 
6
8
  def create_permissions
7
- template "permissions.rb.erb", "app/permissions/#{file_path}.rb", assigns: {
8
- description: description
9
- }
9
+ template(
10
+ 'permissions.rb.erb',
11
+ "app/permissions/#{file_path}.rb",
12
+ assigns: { description: description }
13
+ )
10
14
 
11
- template "permissions_spec.rb.erb", "spec/permissions/#{file_path}_spec.rb"
15
+ template(
16
+ 'permissions_spec.rb.erb',
17
+ "spec/permissions/#{file_path}_spec.rb"
18
+ )
12
19
  end
13
20
  end
14
21
  end
data/renovate.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ]
5
+ }
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Palhares
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-28 00:00:00.000000000 Z
11
+ date: 2021-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
14
+ name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.1.11
19
+ version: 1.17.3
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.1.11
26
+ version: 1.17.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: cancancan
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,47 +39,47 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.15.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: bundler
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.17.3
47
+ version: 12.3.3
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 1.17.3
54
+ version: 12.3.3
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '3.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '3.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rspec
70
+ name: rubocop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '3.0'
75
+ version: 0.65.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '3.0'
82
+ version: 0.65.0
83
83
  description: Consent
84
84
  email:
85
85
  - chjunior@gmail.com
@@ -89,9 +89,12 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - ".gitignore"
91
91
  - ".rspec"
92
+ - ".rubocop.yml"
93
+ - ".rubocop_todo.yml"
92
94
  - ".ruby-version"
93
95
  - ".travis.yml"
94
96
  - Gemfile
97
+ - LICENSE
95
98
  - README.md
96
99
  - Rakefile
97
100
  - TODO.md
@@ -103,8 +106,8 @@ files:
103
106
  - lib/consent/action.rb
104
107
  - lib/consent/dsl.rb
105
108
  - lib/consent/permission.rb
106
- - lib/consent/permissions.rb
107
109
  - lib/consent/railtie.rb
110
+ - lib/consent/reloader.rb
108
111
  - lib/consent/rspec.rb
109
112
  - lib/consent/subject.rb
110
113
  - lib/consent/version.rb
@@ -112,8 +115,10 @@ files:
112
115
  - lib/generators/consent/permissions_generator.rb
113
116
  - lib/generators/consent/templates/permissions.rb.erb
114
117
  - lib/generators/consent/templates/permissions_spec.rb.erb
118
+ - renovate.json
115
119
  homepage:
116
- licenses: []
120
+ licenses:
121
+ - MIT
117
122
  metadata: {}
118
123
  post_install_message:
119
124
  rdoc_options: []
@@ -130,8 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
135
  - !ruby/object:Gem::Version
131
136
  version: '0'
132
137
  requirements: []
133
- rubyforge_project:
134
- rubygems_version: 2.7.3
138
+ rubygems_version: 3.0.8
135
139
  signing_key:
136
140
  specification_version: 4
137
141
  summary: Consent
@@ -1,37 +0,0 @@
1
- module Consent
2
- class Permissions
3
- include Enumerable
4
-
5
- def initialize(permissions)
6
- @permissions = permissions
7
- end
8
-
9
- def each(&block)
10
- Consent.subjects.each do |subject|
11
- subject.actions.map do |action|
12
- map_permission subject, action
13
- end.compact.each(&block)
14
- end
15
- end
16
-
17
- private
18
-
19
- def map_permission(subject, action)
20
- subject_key = subject.permission_key
21
- actions = @permissions[subject_key] || @permissions[subject_key.to_s]
22
- view = actions && (actions[action.key] || actions[action.key.to_s])
23
- full(subject, action, view) || partial(subject, action, view)
24
- end
25
-
26
- def full(subject, action, view_key)
27
- return unless Consent::FULL_ACCESS.include?(view_key.to_s.strip)
28
- Permission.new(subject, action)
29
- end
30
-
31
- def partial(subject, action, view_key)
32
- view = subject.view_for(action, view_key.to_s.to_sym)
33
- return if view.nil?
34
- Permission.new(subject, action, view)
35
- end
36
- end
37
- end