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.
- 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
@@ -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
|
data/lib/consent/reloader.rb
CHANGED
@@ -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
|
11
|
+
def initialize(default_path)
|
10
12
|
@paths = [default_path]
|
11
|
-
@mechanism = mechanism
|
12
13
|
end
|
13
14
|
|
14
|
-
|
15
|
+
private
|
15
16
|
|
16
17
|
def reload!
|
17
18
|
Consent.subjects.clear
|
18
|
-
Consent.load_subjects! paths
|
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
|
-
|
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
|
4
|
-
RSpec::Support.require_rspec_support
|
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
|
38
|
+
failure_message_base "to"
|
39
39
|
end
|
40
40
|
|
41
41
|
def failure_message_when_negated
|
42
|
-
failure_message_base
|
42
|
+
failure_message_base "to not"
|
43
43
|
end
|
44
44
|
|
45
|
-
|
45
|
+
private
|
46
46
|
|
47
47
|
def failure_message_base(failure) # rubocop:disable Metrics/MethodLength
|
48
48
|
message = format(
|
49
|
-
|
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
|
-
|
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
|
-
.
|
31
|
-
|
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
|
36
|
+
failure_message_base "to"
|
40
37
|
end
|
41
38
|
|
42
39
|
def failure_message_when_negated
|
43
|
-
failure_message_base
|
40
|
+
failure_message_base "to not"
|
44
41
|
end
|
45
42
|
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
3
|
+
require "consent"
|
4
4
|
|
5
|
-
require_relative
|
6
|
-
require_relative
|
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
|
data/lib/consent/version.rb
CHANGED
data/lib/consent.rb
CHANGED
@@ -1,17 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
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
|
13
|
-
#
|
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)&.
|
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
|
70
|
-
permission_files = paths.map { |dir| File.join(dir,
|
71
|
-
Dir[*permission_files].each
|
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(
|
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
|
-
|
11
|
-
"app/permissions/#{
|
10
|
+
"permissions.rb.erb",
|
11
|
+
"app/permissions/#{plural_file_name}.rb",
|
12
12
|
assigns: { description: description }
|
13
13
|
)
|
14
14
|
|
15
15
|
template(
|
16
|
-
|
17
|
-
"spec/permissions/#{
|
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
data/renovate.json
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
{
|
2
|
-
"extends": [
|
3
|
-
|
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
|
}
|