consent 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consent
4
+ # Permission migration helper module
5
+ module PermissionMigration
6
+ # Copy permissions from one existing permission into a new permission selecting with
7
+ # attrs would be overrided.
8
+ #
9
+ # I.e.:
10
+ #
11
+ # copy_permissions(
12
+ # from: { subject: :sale, action: :view },
13
+ # override: { subject: :project }
14
+ # )
15
+ #
16
+ # @param from [Hash] a hash with `:subject` and `:action` to select which permissions to copy
17
+ # @param override [Hash] hash to specify which fields/values to override
18
+ #
19
+ def copy_permissions(from:, override:)
20
+ raise ArgumentError, "Subject and Action are always required" if from[:subject].blank? || from[:action].blank?
21
+
22
+ ::Consent::Permission.to(**from).each do |permission|
23
+ ::Consent::Permission.create!(
24
+ permission.slice(:subject, :action, :view, :role_id).merge(override)
25
+ )
26
+ end
27
+ end
28
+
29
+ # Grant a permission to a collection of roles.
30
+ #
31
+ # I.e.:
32
+ #
33
+ # grant_permission(
34
+ # subject: :view_installer_pay_report,
35
+ # action: ProjectTask,
36
+ # role_ids: [2, 7, 140]
37
+ # )
38
+ #
39
+ # @param subject [symbol] the permission's subject
40
+ # @param action [Class, symbol] the permission's action
41
+ # @param role_ids [Array<Integer>] the collection of role_ids to grant the permission to
42
+ # @param view [String, nil] the view level, or access level, that the permission will be
43
+ # assigned at. If not specified, this will default to true ("1")
44
+ #
45
+ def grant_permission(subject:, action:, role_ids:, view: "1")
46
+ role_ids.each do |role_id|
47
+ ::Consent::Permission.create!(subject: subject,
48
+ action: action,
49
+ role_id: role_id,
50
+ view: view)
51
+ end
52
+ end
53
+
54
+ # Removes a permission from a collection of roles.
55
+ #
56
+ # I.e.:
57
+ #
58
+ # remove_permission(
59
+ # subject: :view,
60
+ # action: User,
61
+ # role_ids: [78, 12]
62
+ # )
63
+ #
64
+ # @param subject [symbol] the permission's subject
65
+ # @param action [Class, symbol] the permission's action
66
+ # @param role_ids [Array<Integer>] the collection of role_ids to grant the permission to
67
+ #
68
+ def remove_permission(subject:, action:, role_ids:)
69
+ role_ids.each do |role_id|
70
+ permission = ::Consent::Permission.find_by(subject: subject,
71
+ action: action,
72
+ role_id: role_id)
73
+ permission.destroy!
74
+ end
75
+ end
76
+
77
+ # Batch updates permission data
78
+ #
79
+ # * CAUTION *
80
+ # Updating a permission in a migration means that for some time the old permission
81
+ # will be broken in production. So, you might lock out people between the permission
82
+ # running and your code getting deployed/restarted in the webservers.
83
+ #
84
+ # Example:
85
+ # - Page A is only displayed to users that `can? :view, Candidate`
86
+ # - If you're willing to rename the `view` action to be `view_candidates`
87
+ # - Then you could go with a permission like this
88
+ # update_permissions(
89
+ # from: { subject: :candidate, action: :view },
90
+ # to: { action: :view_candidates }
91
+ # )
92
+ # - And you'll have to change the permission check to be `can? :view_candidates, Candidate`
93
+ # - When you merge your PR, then the migration will run first, and later on your code will
94
+ # reach production.
95
+ # - Between that time, the page that uses that permission will be unreachable since
96
+ # `can? :view, Candidate` doesn't exists anymore in the DB.
97
+ #
98
+ # I.e.:
99
+ #
100
+ # Renames a subject affecting all grantted permissions keeping everything else
101
+ #
102
+ # update_permissions(
103
+ # from: { subject: :sale },
104
+ # to: { subject: :project }
105
+ # )
106
+ #
107
+ # Moves an action from a subject to another keeping the view
108
+ #
109
+ # update_permissions(
110
+ # from: { subject: :sale, action: :perform },
111
+ # to: { subject: :project }
112
+ # )
113
+ #
114
+ # Rename an action within a subject keeping the view
115
+ #
116
+ # update_permissions(
117
+ # from: { subject: :sale, action: :read },
118
+ # to: { action: :inspect }
119
+ # )
120
+ #
121
+ # Rename a view within a subject and action context
122
+ #
123
+ # update_permissions(
124
+ # from: { subject: :sale, action: :read, view: :territory },
125
+ # to: { view: :department_territory }
126
+ # )
127
+ #
128
+ # @param from [Hash] a hash with `:subject`, `:action`, and `:view` to match the affected permissions
129
+ # @param to [Hash] a hash with `:subject`, `:action`, and/or `:view` with the desired change
130
+ #
131
+ def update_permissions(from:, to:)
132
+ raise ArgumentError, "Subject is always required" if from[:subject].blank?
133
+
134
+ ::Consent::Permission.to(**from).find_each do |permission|
135
+ permission.update(to)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -2,20 +2,21 @@
2
2
 
3
3
  module Consent
4
4
  # Rails file reloader to detect permission changes and apply them to consent
5
+ # @private
5
6
  class Reloader
6
7
  attr_reader :paths
8
+
7
9
  delegate :updated?, :execute, :execute_if_updated, to: :updater
8
10
 
9
- def initialize(default_path, mechanism)
11
+ def initialize(default_path)
10
12
  @paths = [default_path]
11
- @mechanism = mechanism
12
13
  end
13
14
 
14
- private
15
+ private
15
16
 
16
17
  def reload!
17
18
  Consent.subjects.clear
18
- Consent.load_subjects! paths, @mechanism
19
+ Consent.load_subjects! paths
19
20
  end
20
21
 
21
22
  def updater
@@ -24,7 +25,7 @@ module Consent
24
25
 
25
26
  def globs
26
27
  pairs = paths.map { |path| [path.to_s, %w[rb]] }
27
- Hash[pairs]
28
+ pairs.to_h
28
29
  end
29
30
  end
30
31
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'consent'
4
- RSpec::Support.require_rspec_support 'fuzzy_matcher'
3
+ require "consent"
4
+ RSpec::Support.require_rspec_support "fuzzy_matcher"
5
5
 
6
6
  module Consent
7
7
  module Rspec
@@ -35,25 +35,25 @@ module Consent
35
35
  end
36
36
 
37
37
  def failure_message
38
- failure_message_base 'to'
38
+ failure_message_base "to"
39
39
  end
40
40
 
41
41
  def failure_message_when_negated
42
- failure_message_base 'to not'
42
+ failure_message_base "to not"
43
43
  end
44
44
 
45
- private
45
+ private
46
46
 
47
47
  def failure_message_base(failure) # rubocop:disable Metrics/MethodLength
48
48
  message = format(
49
- 'expected %<skey>s (%<sclass>s) %<failure> provide action %<action>s',
49
+ "expected %<skey>s (%<sclass>s) %<failure> provide action %<action>s",
50
50
  skey: @subject_key.to_s, sclass: @subject_key.class,
51
51
  action: @action_key, failure: failure
52
52
  )
53
53
 
54
54
  if @action && @views
55
55
  format(
56
- '%<message>s with views %<views>s, but actual views are %<keys>p',
56
+ "%<message>s with views %<views>s, but actual views are %<keys>p",
57
57
  message: message, views: @views, keys: @action.views.keys
58
58
  )
59
59
  else
@@ -27,23 +27,20 @@ module Consent
27
27
  def matches?(subject_key)
28
28
  @subject_key = subject_key
29
29
  @target = Consent.find_subjects(subject_key)
30
- .map do |subject|
31
- subject.views[@view_key]&.conditions(*@context)
32
- end
33
- .compact
34
- .map(&method(:comparable_conditions))
30
+ .filter_map { |subject| subject.views[@view_key]&.conditions(*@context) }
31
+ .map { |c| comparable_conditions(c) }
35
32
  @target.include?(@conditions)
36
33
  end
37
34
 
38
35
  def failure_message
39
- failure_message_base 'to'
36
+ failure_message_base "to"
40
37
  end
41
38
 
42
39
  def failure_message_when_negated
43
- failure_message_base 'to not'
40
+ failure_message_base "to not"
44
41
  end
45
42
 
46
- private
43
+ private
47
44
 
48
45
  def comparable_conditions(conditions)
49
46
  return conditions.to_sql if conditions.respond_to?(:to_sql)
@@ -61,15 +58,15 @@ module Consent
61
58
 
62
59
  if @target.any?
63
60
  format(
64
- '%<message>s conditions are %<conditions>p',
61
+ "%<message>s conditions are %<conditions>p",
65
62
  message: message, conditions: @target
66
63
  )
67
64
  else
68
- actual_views = Consent.find_subjects(subject_key)
65
+ actual_views = Consent.find_subjects(@subject_key)
69
66
  .map(&:views)
70
67
  .map(&:keys).flatten
71
68
  format(
72
- '%<message>s available views are %<views>p',
69
+ "%<message>s available views are %<views>p",
73
70
  message: message, views: actual_views
74
71
  )
75
72
  end
data/lib/consent/rspec.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'consent'
3
+ require "consent"
4
4
 
5
- require_relative 'rspec/consent_action'
6
- require_relative 'rspec/consent_view'
5
+ require_relative "rspec/consent_action"
6
+ require_relative "rspec/consent_view"
7
7
 
8
8
  module Consent
9
9
  # RSpec helpers for consent. Given permissions are loaded,
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consent
4
+ # Coder for ability subjects
5
+ module SubjectCoder
6
+ module_function
7
+
8
+ # Loads the serialized key (snake case string) as a valid
9
+ # permission subject (a constant or a symbol)
10
+ #
11
+ # @param key [String] snake case string representing a subject
12
+ # @return [Module,Class,Symbol]
13
+ def load(key)
14
+ return nil unless key
15
+
16
+ constant = key.camelize.safe_constantize
17
+ constant.is_a?(Class) ? constant : key.to_sym
18
+ end
19
+
20
+ # Dumps a serialized key (snake case string) from a valid
21
+ # permission subject (a constant or a symbol)
22
+ #
23
+ # @param key [Module,Class,Symbol] the subject key
24
+ # @return [String]
25
+ def dump(key)
26
+ key.to_s.underscore
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanYouReally
4
+ # @private
5
+ class SymbolAdapter < ::CanCan::ModelAdapters::AbstractAdapter
6
+ def self.for_class?(subject)
7
+ subject.is_a?(Symbol) || subject == Symbol
8
+ end
9
+
10
+ def self.override_conditions_hash_matching?(_subject, _conditions)
11
+ true
12
+ end
13
+
14
+ def self.matches_conditions_hash?(_subject, _conditions)
15
+ true
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Consent
4
- VERSION = '1.0.1'
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/consent.rb CHANGED
@@ -1,17 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'consent/version'
4
- require 'consent/subject'
5
- require 'consent/view'
6
- require 'consent/action'
7
- require 'consent/dsl'
8
- require 'consent/ability' if defined?(CanCan)
9
- require 'consent/railtie' if defined?(Rails)
3
+ require "cancancan"
4
+
5
+ require "consent/ability"
6
+ require "consent/action"
7
+ require "consent/dsl"
8
+ require "consent/model_additions"
9
+ require "consent/reloader"
10
+ require "consent/subject_coder"
11
+ require "consent/subject"
12
+ require "consent/symbol_adapter"
13
+ require "consent/version"
14
+ require "consent/view"
10
15
 
11
16
  # Consent makes defining permissions easier by providing a clean,
12
- # concise DSL for authorization so that all abilities do not have
13
- # to be in your `Ability` class.
17
+ # concise DSL for authorization so that your `Ability` isn't bloated
18
+ # with all logic.
14
19
  module Consent
20
+ FULL_ACCESS = %w[1 true].freeze
21
+ NO_ACCESS = :no_access
22
+
23
+ # Custom table_name_prefix for the Consent tables.
24
+ # Default: "consent_"
25
+ cattr_accessor(:table_name_prefix) { "consent_" }
26
+
15
27
  ViewNotFound = Class.new(StandardError)
16
28
 
17
29
  # Default views available to every permission
@@ -56,7 +68,7 @@ module Consent
56
68
  #
57
69
  # @return [Consent::View,nil]
58
70
  def self.find_view(subject_key, action_key, view_key)
59
- find_action(subject_key, action_key)&.yield_self do |action|
71
+ find_action(subject_key, action_key)&.then do |action|
60
72
  action.views[view_key] || raise(Consent::ViewNotFound)
61
73
  end
62
74
  end
@@ -66,9 +78,9 @@ module Consent
66
78
  #
67
79
  # @param paths [Array<String,#to_s>] paths where the ruby files are located
68
80
  # @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))
81
+ def self.load_subjects!(paths)
82
+ permission_files = paths.map { |dir| File.join(dir, "*.rb") }
83
+ Dir[*permission_files].each { |file| Kernel.load(file) }
72
84
  end
73
85
 
74
86
  # Defines a subject with the given key, label and options
@@ -2,19 +2,19 @@
2
2
 
3
3
  module Consent
4
4
  class PermissionsGenerator < Rails::Generators::NamedBase # :nodoc:
5
- source_root File.expand_path('templates', __dir__)
5
+ source_root File.expand_path("templates", __dir__)
6
6
  argument :description, type: :string, required: false
7
7
 
8
8
  def create_permissions
9
9
  template(
10
- 'permissions.rb.erb',
11
- "app/permissions/#{file_path}.rb",
10
+ "permissions.rb.erb",
11
+ "app/permissions/#{plural_file_name}.rb",
12
12
  assigns: { description: description }
13
13
  )
14
14
 
15
15
  template(
16
- 'permissions_spec.rb.erb',
17
- "spec/permissions/#{file_path}_spec.rb"
16
+ "permissions_spec.rb.erb",
17
+ "spec/permissions/#{plural_file_name}_spec.rb"
18
18
  )
19
19
  end
20
20
  end
data/mkdocs.yml ADDED
@@ -0,0 +1,5 @@
1
+ site_name: Consent
2
+ nav:
3
+ - "Home": "README.md"
4
+ plugins:
5
+ - techdocs-core
data/renovate.json CHANGED
@@ -1,5 +1,18 @@
1
1
  {
2
- "extends": [
3
- "config:base"
2
+ "extends": ["config:base", "group:allNonMajor"],
3
+ "lockFileMaintenance": {
4
+ "enabled": true
5
+ },
6
+ "labels": ["dependencies"],
7
+ "timezone": "America/New_York",
8
+ "packageRules": [
9
+ {
10
+ "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
11
+ "automerge": true
12
+ },
13
+ {
14
+ "matchDepTypes": ["devDependencies"],
15
+ "automerge": true
16
+ }
4
17
  ]
5
18
  }