pundit 2.3.2 → 2.5.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/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +26 -0
- data/.github/PULL_REQUEST_TEMPLATE/gem_release_template.md +4 -4
- data/.github/workflows/main.yml +92 -52
- data/.github/workflows/push_gem.yml +4 -4
- data/.rubocop.yml +18 -8
- data/.rubocop_ignore_git.yml +7 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +68 -37
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +1 -0
- data/Gemfile +22 -2
- data/README.md +88 -15
- data/Rakefile +1 -0
- data/lib/generators/pundit/install/install_generator.rb +3 -1
- data/lib/generators/pundit/policy/policy_generator.rb +3 -1
- data/lib/generators/rspec/policy_generator.rb +4 -1
- data/lib/generators/test_unit/policy_generator.rb +4 -1
- data/lib/pundit/authorization.rb +152 -77
- data/lib/pundit/cache_store/legacy_store.rb +7 -0
- data/lib/pundit/cache_store/null_store.rb +9 -0
- data/lib/pundit/cache_store.rb +22 -0
- data/lib/pundit/context.rb +76 -26
- data/lib/pundit/policy_finder.rb +22 -1
- data/lib/pundit/railtie.rb +19 -0
- data/lib/pundit/rspec.rb +90 -7
- data/lib/pundit/version.rb +2 -1
- data/lib/pundit.rb +43 -15
- data/pundit.gemspec +8 -12
- data/spec/authorization_spec.rb +61 -4
- data/spec/policies/post_policy_spec.rb +27 -0
- data/spec/policy_finder_spec.rb +5 -1
- data/spec/pundit/helper_spec.rb +18 -0
- data/spec/pundit_spec.rb +58 -15
- data/spec/rspec_dsl_spec.rb +81 -0
- data/spec/simple_cov_check_action_formatter.rb +79 -0
- data/spec/spec_helper.rb +22 -339
- data/spec/support/lib/controller.rb +38 -0
- data/spec/support/lib/custom_cache.rb +19 -0
- data/spec/support/lib/instance_tracking.rb +20 -0
- data/spec/support/models/article.rb +4 -0
- data/spec/support/models/article_tag.rb +7 -0
- data/spec/support/models/artificial_blog.rb +7 -0
- data/spec/support/models/blog.rb +4 -0
- data/spec/support/models/comment.rb +5 -0
- data/spec/support/models/comment_four_five_six.rb +5 -0
- data/spec/support/models/comment_scope.rb +13 -0
- data/spec/support/models/comments_relation.rb +15 -0
- data/spec/support/models/customer/post.rb +11 -0
- data/spec/support/models/default_scope_contains_error.rb +5 -0
- data/spec/support/models/dummy_current_user.rb +7 -0
- data/spec/support/models/foo.rb +4 -0
- data/spec/support/models/post.rb +25 -0
- data/spec/support/models/post_four_five_six.rb +9 -0
- data/spec/support/models/project_one_two_three/avatar_four_five_six.rb +7 -0
- data/spec/support/models/project_one_two_three/tag_four_five_six.rb +11 -0
- data/spec/support/models/wiki.rb +4 -0
- data/spec/support/policies/article_tag_other_name_policy.rb +13 -0
- data/spec/support/policies/base_policy.rb +23 -0
- data/spec/support/policies/blog_policy.rb +5 -0
- data/spec/support/policies/comment_policy.rb +11 -0
- data/spec/support/policies/criteria_policy.rb +5 -0
- data/spec/support/policies/default_scope_contains_error_policy.rb +10 -0
- data/spec/support/policies/denier_policy.rb +7 -0
- data/spec/support/policies/dummy_current_user_policy.rb +9 -0
- data/spec/support/policies/nil_class_policy.rb +17 -0
- data/spec/support/policies/post_policy.rb +36 -0
- data/spec/support/policies/project/admin/comment_policy.rb +15 -0
- data/spec/support/policies/project/comment_policy.rb +17 -0
- data/spec/support/policies/project/criteria_policy.rb +7 -0
- data/spec/support/policies/project/post_policy.rb +13 -0
- data/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb +6 -0
- data/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb +6 -0
- data/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb +6 -0
- data/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb +6 -0
- data/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb +6 -0
- data/spec/support/policies/publication_policy.rb +13 -0
- data/spec/support/policies/wiki_policy.rb +8 -0
- metadata +66 -158
- /data/.github/{PULL_REQUEST_TEMPLATE/pull_request_template.md → pull_request_template.md} +0 -0
- /data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
- /data/lib/generators/pundit/policy/templates/{policy.rb → policy.rb.tt} +0 -0
- /data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
- /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
data/lib/pundit/context.rb
CHANGED
@@ -1,22 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Pundit
|
4
|
+
# {Pundit::Context} is intended to be created once per request and user, and
|
5
|
+
# it is then used to perform authorization checks throughout the request.
|
6
|
+
#
|
7
|
+
# @example Using Sinatra
|
8
|
+
# helpers do
|
9
|
+
# def current_user = ...
|
10
|
+
#
|
11
|
+
# def pundit
|
12
|
+
# @pundit ||= Pundit::Context.new(user: current_user)
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# get "/posts/:id" do |id|
|
17
|
+
# pundit.authorize(Post.find(id), query: :show?)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Using [Roda](https://roda.jeremyevans.net/index.html)
|
21
|
+
# route do |r|
|
22
|
+
# context = Pundit::Context.new(user:)
|
23
|
+
#
|
24
|
+
# r.get "posts", Integer do |id|
|
25
|
+
# context.authorize(Post.find(id), query: :show?)
|
26
|
+
# end
|
27
|
+
# end
|
4
28
|
class Context
|
29
|
+
# @see Pundit::Authorization#pundit
|
30
|
+
# @param user later passed to policies and scopes
|
31
|
+
# @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore})
|
5
32
|
def initialize(user:, policy_cache: CacheStore::NullStore.instance)
|
6
33
|
@user = user
|
7
34
|
@policy_cache = policy_cache
|
8
35
|
end
|
9
36
|
|
37
|
+
# @api public
|
38
|
+
# @see #initialize
|
10
39
|
attr_reader :user
|
11
40
|
|
12
41
|
# @api private
|
42
|
+
# @see #initialize
|
13
43
|
attr_reader :policy_cache
|
14
44
|
|
45
|
+
# @!group Policies
|
46
|
+
|
15
47
|
# Retrieves the policy for the given record, initializing it with the
|
16
48
|
# record and user and finally throwing an error if the user is not
|
17
49
|
# authorized to perform the given action.
|
18
50
|
#
|
19
|
-
# @param user [Object] the user that initiated the action
|
20
51
|
# @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
|
21
52
|
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
|
22
53
|
# @param policy_class [Class] the policy class we want to force use of
|
@@ -35,10 +66,34 @@ module Pundit
|
|
35
66
|
record
|
36
67
|
end
|
37
68
|
|
69
|
+
# Retrieves the policy for the given record.
|
70
|
+
#
|
71
|
+
# @see https://github.com/varvet/pundit#policies
|
72
|
+
# @param record [Object] the object we're retrieving the policy for
|
73
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
74
|
+
# @return [Object, nil] instance of policy class with query methods
|
75
|
+
def policy(record)
|
76
|
+
cached_find(record, &:policy)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Retrieves the policy for the given record, or raises if not found.
|
80
|
+
#
|
81
|
+
# @see https://github.com/varvet/pundit#policies
|
82
|
+
# @param record [Object] the object we're retrieving the policy for
|
83
|
+
# @raise [NotDefinedError] if the policy cannot be found
|
84
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
85
|
+
# @return [Object] instance of policy class with query methods
|
86
|
+
def policy!(record)
|
87
|
+
cached_find(record, &:policy!)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @!endgroup
|
91
|
+
|
92
|
+
# @!group Scopes
|
93
|
+
|
38
94
|
# Retrieves the policy scope for the given record.
|
39
95
|
#
|
40
96
|
# @see https://github.com/varvet/pundit#scopes
|
41
|
-
# @param user [Object] the user that initiated the action
|
42
97
|
# @param scope [Object] the object we're retrieving the policy scope for
|
43
98
|
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
44
99
|
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
|
@@ -58,14 +113,12 @@ module Pundit
|
|
58
113
|
# Retrieves the policy scope for the given record. Raises if not found.
|
59
114
|
#
|
60
115
|
# @see https://github.com/varvet/pundit#scopes
|
61
|
-
# @param user [Object] the user that initiated the action
|
62
116
|
# @param scope [Object] the object we're retrieving the policy scope for
|
63
117
|
# @raise [NotDefinedError] if the policy scope cannot be found
|
64
118
|
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
65
119
|
# @return [Scope{#resolve}] instance of scope class which can resolve to a scope
|
66
120
|
def policy_scope!(scope)
|
67
121
|
policy_scope_class = policy_finder(scope).scope!
|
68
|
-
return unless policy_scope_class
|
69
122
|
|
70
123
|
begin
|
71
124
|
policy_scope = policy_scope_class.new(user, pundit_model(scope))
|
@@ -76,31 +129,21 @@ module Pundit
|
|
76
129
|
policy_scope.resolve
|
77
130
|
end
|
78
131
|
|
79
|
-
#
|
80
|
-
#
|
81
|
-
# @see https://github.com/varvet/pundit#policies
|
82
|
-
# @param user [Object] the user that initiated the action
|
83
|
-
# @param record [Object] the object we're retrieving the policy for
|
84
|
-
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
85
|
-
# @return [Object, nil] instance of policy class with query methods
|
86
|
-
def policy(record)
|
87
|
-
cached_find(record, &:policy)
|
88
|
-
end
|
89
|
-
|
90
|
-
# Retrieves the policy for the given record. Raises if not found.
|
91
|
-
#
|
92
|
-
# @see https://github.com/varvet/pundit#policies
|
93
|
-
# @param user [Object] the user that initiated the action
|
94
|
-
# @param record [Object] the object we're retrieving the policy for
|
95
|
-
# @raise [NotDefinedError] if the policy cannot be found
|
96
|
-
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
97
|
-
# @return [Object] instance of policy class with query methods
|
98
|
-
def policy!(record)
|
99
|
-
cached_find(record, &:policy!)
|
100
|
-
end
|
132
|
+
# @!endgroup
|
101
133
|
|
102
134
|
private
|
103
135
|
|
136
|
+
# @!group Private Helpers
|
137
|
+
|
138
|
+
# Finds a cached policy for the given record, or yields to find one.
|
139
|
+
#
|
140
|
+
# @api private
|
141
|
+
# @param record [Object] the object we're retrieving the policy for
|
142
|
+
# @yield a policy finder if no policy was cached
|
143
|
+
# @yieldparam [PolicyFinder] policy_finder
|
144
|
+
# @yieldreturn [#new(user, model)]
|
145
|
+
# @return [Policy, nil] an instantiated policy
|
146
|
+
# @raise [InvalidConstructorError] if policy can't be instantated
|
104
147
|
def cached_find(record)
|
105
148
|
policy_cache.fetch(user: user, record: record) do
|
106
149
|
klass = yield policy_finder(record)
|
@@ -116,10 +159,17 @@ module Pundit
|
|
116
159
|
end
|
117
160
|
end
|
118
161
|
|
162
|
+
# Return a policy finder for the given record.
|
163
|
+
#
|
164
|
+
# @api private
|
165
|
+
# @return [PolicyFinder]
|
119
166
|
def policy_finder(record)
|
120
167
|
PolicyFinder.new(record)
|
121
168
|
end
|
122
169
|
|
170
|
+
# Given a possibly namespaced record, return the actual record.
|
171
|
+
#
|
172
|
+
# @api private
|
123
173
|
def pundit_model(record)
|
124
174
|
record.is_a?(Array) ? record.last : record
|
125
175
|
end
|
data/lib/pundit/policy_finder.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# String#safe_constantize, String#demodulize, String#underscore, String#camelize
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
|
3
6
|
module Pundit
|
4
7
|
# Finds policy and scope classes for given object.
|
5
8
|
# @api public
|
@@ -10,10 +13,15 @@ module Pundit
|
|
10
13
|
# finder.scope #=> UserPolicy::Scope
|
11
14
|
#
|
12
15
|
class PolicyFinder
|
16
|
+
# A constant applied to the end of the class name to find the policy class.
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
SUFFIX = "Policy"
|
20
|
+
|
21
|
+
# @see #initialize
|
13
22
|
attr_reader :object
|
14
23
|
|
15
24
|
# @param object [any] the object to find policy and scope classes for
|
16
|
-
#
|
17
25
|
def initialize(object)
|
18
26
|
@object = object
|
19
27
|
end
|
@@ -70,6 +78,11 @@ module Pundit
|
|
70
78
|
|
71
79
|
private
|
72
80
|
|
81
|
+
# Given an object, find the policy class name.
|
82
|
+
#
|
83
|
+
# Uses recursion to handle namespaces.
|
84
|
+
#
|
85
|
+
# @return [String, Class] the policy class, or its name.
|
73
86
|
def find(subject)
|
74
87
|
if subject.is_a?(Array)
|
75
88
|
modules = subject.dup
|
@@ -86,6 +99,14 @@ module Pundit
|
|
86
99
|
end
|
87
100
|
end
|
88
101
|
|
102
|
+
# Given an object, find its' class name.
|
103
|
+
#
|
104
|
+
# - Supports ActiveModel.
|
105
|
+
# - Supports regular classes.
|
106
|
+
# - Supports symbols.
|
107
|
+
# - Supports object instances.
|
108
|
+
#
|
109
|
+
# @return [String, Class] the class, or its name.
|
89
110
|
def find_class_name(subject)
|
90
111
|
if subject.respond_to?(:model_name)
|
91
112
|
subject.model_name
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pundit
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
if Rails.version.to_f >= 8.0
|
6
|
+
initializer "pundit.stats_directories" do
|
7
|
+
require "rails/code_statistics"
|
8
|
+
|
9
|
+
if Rails.root.join("app/policies").directory?
|
10
|
+
Rails::CodeStatistics.register_directory("Policies", "app/policies")
|
11
|
+
end
|
12
|
+
|
13
|
+
if Rails.root.join("test/policies").directory?
|
14
|
+
Rails::CodeStatistics.register_directory("Policy tests", "test/policies", test_directory: true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/pundit/rspec.rb
CHANGED
@@ -1,10 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Array#to_sentence
|
4
|
+
require "active_support/core_ext/array/conversions"
|
5
|
+
|
3
6
|
module Pundit
|
7
|
+
# Namespace for Pundit's RSpec integration.
|
4
8
|
module RSpec
|
9
|
+
# Namespace for Pundit's RSpec matchers.
|
5
10
|
module Matchers
|
6
11
|
extend ::RSpec::Matchers::DSL
|
7
12
|
|
13
|
+
# @!method description=(description)
|
14
|
+
class << self
|
15
|
+
# Used to build a suitable description for the Pundit `permit` matcher.
|
16
|
+
# @api public
|
17
|
+
# @param value [String, Proc]
|
18
|
+
# @example
|
19
|
+
# Pundit::RSpec::Matchers.description = ->(user, record) do
|
20
|
+
# "permit user with role #{user.role} to access record with ID #{record.id}"
|
21
|
+
# end
|
22
|
+
attr_writer :description
|
23
|
+
|
24
|
+
# Used to retrieve a suitable description for the Pundit `permit` matcher.
|
25
|
+
# @api private
|
26
|
+
# @private
|
27
|
+
def description(user, record)
|
28
|
+
return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
|
29
|
+
|
30
|
+
@description
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
8
34
|
# rubocop:disable Metrics/BlockLength
|
9
35
|
matcher :permit do |user, record|
|
10
36
|
match_proc = lambda do |policy|
|
@@ -22,15 +48,25 @@ module Pundit
|
|
22
48
|
end
|
23
49
|
|
24
50
|
failure_message_proc = lambda do |policy|
|
25
|
-
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
26
51
|
"Expected #{policy} to grant #{permissions.to_sentence} on " \
|
27
|
-
"#{record} but #{@violating_permissions.to_sentence} #{
|
52
|
+
"#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted"
|
28
53
|
end
|
29
54
|
|
30
55
|
failure_message_when_negated_proc = lambda do |policy|
|
31
|
-
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
32
56
|
"Expected #{policy} not to grant #{permissions.to_sentence} on " \
|
33
|
-
"#{record} but #{@violating_permissions.to_sentence} #{
|
57
|
+
"#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted"
|
58
|
+
end
|
59
|
+
|
60
|
+
def was_or_were
|
61
|
+
if @violating_permissions.count > 1
|
62
|
+
"were"
|
63
|
+
else
|
64
|
+
"was"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
description do
|
69
|
+
Pundit::RSpec::Matchers.description(user, record) || super()
|
34
70
|
end
|
35
71
|
|
36
72
|
if respond_to?(:match_when_negated)
|
@@ -39,26 +75,73 @@ module Pundit
|
|
39
75
|
failure_message(&failure_message_proc)
|
40
76
|
failure_message_when_negated(&failure_message_when_negated_proc)
|
41
77
|
else
|
78
|
+
# :nocov:
|
79
|
+
# Compatibility with RSpec < 3.0, released 2014-06-01.
|
42
80
|
match_for_should(&match_proc)
|
43
81
|
match_for_should_not(&match_when_negated_proc)
|
44
82
|
failure_message_for_should(&failure_message_proc)
|
45
83
|
failure_message_for_should_not(&failure_message_when_negated_proc)
|
84
|
+
# :nocov:
|
85
|
+
end
|
86
|
+
|
87
|
+
if ::RSpec.respond_to?(:current_example)
|
88
|
+
def current_example
|
89
|
+
::RSpec.current_example
|
90
|
+
end
|
91
|
+
else
|
92
|
+
# :nocov:
|
93
|
+
# Compatibility with RSpec < 3.0, released 2014-06-01.
|
94
|
+
def current_example
|
95
|
+
example
|
96
|
+
end
|
97
|
+
# :nocov:
|
46
98
|
end
|
47
99
|
|
48
100
|
def permissions
|
49
|
-
current_example
|
50
|
-
|
101
|
+
current_example.metadata.fetch(:permissions) do
|
102
|
+
raise KeyError, <<~ERROR.strip
|
103
|
+
No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
|
104
|
+
ERROR
|
105
|
+
end
|
51
106
|
end
|
52
107
|
end
|
53
108
|
# rubocop:enable Metrics/BlockLength
|
54
109
|
end
|
55
110
|
|
111
|
+
# Mixed in to all policy example groups to provide a DSL.
|
56
112
|
module DSL
|
113
|
+
# @example
|
114
|
+
# describe PostPolicy do
|
115
|
+
# permissions :show?, :update? do
|
116
|
+
# it { is_expected.to permit(user, own_post) }
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# @example focused example group
|
121
|
+
# describe PostPolicy do
|
122
|
+
# permissions :show?, :update?, :focus do
|
123
|
+
# it { is_expected.to permit(user, own_post) }
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# @param list [Symbol, Array<Symbol>] a permission to describe
|
128
|
+
# @return [void]
|
57
129
|
def permissions(*list, &block)
|
58
|
-
|
130
|
+
metadata = { permissions: list, caller: caller }
|
131
|
+
|
132
|
+
if list.last == :focus
|
133
|
+
list.pop
|
134
|
+
metadata[:focus] = true
|
135
|
+
end
|
136
|
+
|
137
|
+
description = list.to_sentence
|
138
|
+
describe(description, metadata) { instance_eval(&block) }
|
59
139
|
end
|
60
140
|
end
|
61
141
|
|
142
|
+
# Mixed in to all policy example groups.
|
143
|
+
#
|
144
|
+
# @private not useful
|
62
145
|
module PolicyExampleGroup
|
63
146
|
include Pundit::RSpec::Matchers
|
64
147
|
|
data/lib/pundit/version.rb
CHANGED
data/lib/pundit.rb
CHANGED
@@ -1,33 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
|
3
5
|
require "pundit/version"
|
4
6
|
require "pundit/policy_finder"
|
5
|
-
require "active_support/concern"
|
6
|
-
require "active_support/core_ext/string/inflections"
|
7
|
-
require "active_support/core_ext/object/blank"
|
8
|
-
require "active_support/core_ext/module/introspection"
|
9
|
-
require "active_support/dependencies/autoload"
|
10
7
|
require "pundit/authorization"
|
11
8
|
require "pundit/context"
|
9
|
+
require "pundit/cache_store"
|
12
10
|
require "pundit/cache_store/null_store"
|
13
11
|
require "pundit/cache_store/legacy_store"
|
12
|
+
require "pundit/railtie" if defined?(Rails)
|
14
13
|
|
15
14
|
# @api private
|
16
15
|
# To avoid name clashes with common Error naming when mixing in Pundit,
|
17
16
|
# keep it here with compact class style definition.
|
18
17
|
class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren
|
19
18
|
|
19
|
+
# Hello? Yes, this is Pundit.
|
20
|
+
#
|
20
21
|
# @api public
|
21
22
|
module Pundit
|
22
|
-
|
23
|
+
# @api private
|
24
|
+
# @deprecated See {Pundit::PolicyFinder}
|
25
|
+
SUFFIX = Pundit::PolicyFinder::SUFFIX
|
23
26
|
|
24
27
|
# @api private
|
28
|
+
# @private
|
25
29
|
module Generators; end
|
26
30
|
|
27
31
|
# Error that will be raised when authorization has failed
|
28
32
|
class NotAuthorizedError < Error
|
29
|
-
|
30
|
-
|
33
|
+
# @see #initialize
|
34
|
+
attr_reader :query
|
35
|
+
# @see #initialize
|
36
|
+
attr_reader :record
|
37
|
+
# @see #initialize
|
38
|
+
attr_reader :policy
|
39
|
+
|
40
|
+
# @overload initialize(message)
|
41
|
+
# Create an error with a simple error message.
|
42
|
+
# @param [String] message A simple error message string.
|
43
|
+
#
|
44
|
+
# @overload initialize(options)
|
45
|
+
# Create an error with the specified attributes.
|
46
|
+
# @param [Hash] options The error options.
|
47
|
+
# @option options [String] :message Optional custom error message. Will default to a generalized message.
|
48
|
+
# @option options [Symbol] :query The name of the policy method that was checked.
|
49
|
+
# @option options [Object] :record The object that was being checked with the policy.
|
50
|
+
# @option options [Class] :policy The class of policy that was used for the check.
|
31
51
|
def initialize(options = {})
|
32
52
|
if options.is_a? String
|
33
53
|
message = options
|
@@ -36,7 +56,10 @@ module Pundit
|
|
36
56
|
@record = options[:record]
|
37
57
|
@policy = options[:policy]
|
38
58
|
|
39
|
-
message = options.fetch(:message)
|
59
|
+
message = options.fetch(:message) do
|
60
|
+
record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}"
|
61
|
+
"not allowed to #{policy.class}##{query} #{record_name}"
|
62
|
+
end
|
40
63
|
end
|
41
64
|
|
42
65
|
super(message)
|
@@ -67,10 +90,11 @@ module Pundit
|
|
67
90
|
end
|
68
91
|
|
69
92
|
class << self
|
70
|
-
# @see
|
93
|
+
# @see Pundit::Context#authorize
|
71
94
|
def authorize(user, record, query, policy_class: nil, cache: nil)
|
72
95
|
context = if cache
|
73
|
-
|
96
|
+
policy_cache = CacheStore::LegacyStore.new(cache)
|
97
|
+
Context.new(user: user, policy_cache: policy_cache)
|
74
98
|
else
|
75
99
|
Context.new(user: user)
|
76
100
|
end
|
@@ -78,29 +102,33 @@ module Pundit
|
|
78
102
|
context.authorize(record, query: query, policy_class: policy_class)
|
79
103
|
end
|
80
104
|
|
81
|
-
# @see
|
105
|
+
# @see Pundit::Context#policy_scope
|
82
106
|
def policy_scope(user, *args, **kwargs, &block)
|
83
107
|
Context.new(user: user).policy_scope(*args, **kwargs, &block)
|
84
108
|
end
|
85
109
|
|
86
|
-
# @see
|
110
|
+
# @see Pundit::Context#policy_scope!
|
87
111
|
def policy_scope!(user, *args, **kwargs, &block)
|
88
112
|
Context.new(user: user).policy_scope!(*args, **kwargs, &block)
|
89
113
|
end
|
90
114
|
|
91
|
-
# @see
|
115
|
+
# @see Pundit::Context#policy
|
92
116
|
def policy(user, *args, **kwargs, &block)
|
93
117
|
Context.new(user: user).policy(*args, **kwargs, &block)
|
94
118
|
end
|
95
119
|
|
96
|
-
# @see
|
120
|
+
# @see Pundit::Context#policy!
|
97
121
|
def policy!(user, *args, **kwargs, &block)
|
98
122
|
Context.new(user: user).policy!(*args, **kwargs, &block)
|
99
123
|
end
|
100
124
|
end
|
101
125
|
|
126
|
+
# Rails view helpers, to allow a slightly different view-specific
|
127
|
+
# implementation of the methods in {Pundit::Authorization}.
|
128
|
+
#
|
102
129
|
# @api private
|
103
130
|
module Helper
|
131
|
+
# @see Pundit::Authorization#pundit_policy_scope
|
104
132
|
def policy_scope(scope)
|
105
133
|
pundit_policy_scope(scope)
|
106
134
|
end
|
data/pundit.gemspec
CHANGED
@@ -16,20 +16,16 @@ Gem::Specification.new do |gem|
|
|
16
16
|
|
17
17
|
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
18
18
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
19
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
19
|
gem.require_paths = ["lib"]
|
21
20
|
|
22
|
-
gem.metadata = {
|
21
|
+
gem.metadata = {
|
22
|
+
"rubygems_mfa_required" => "true",
|
23
|
+
"bug_tracker_uri" => "https://github.com/varvet/pundit/issues",
|
24
|
+
"changelog_uri" => "https://github.com/varvet/pundit/blob/main/CHANGELOG.md",
|
25
|
+
"documentation_uri" => "https://github.com/varvet/pundit/blob/main/README.md",
|
26
|
+
"homepage_uri" => "https://github.com/varvet/pundit",
|
27
|
+
"source_code_uri" => "https://github.com/varvet/pundit"
|
28
|
+
}
|
23
29
|
|
24
30
|
gem.add_dependency "activesupport", ">= 3.0.0"
|
25
|
-
gem.add_development_dependency "actionpack", ">= 3.0.0"
|
26
|
-
gem.add_development_dependency "activemodel", ">= 3.0.0"
|
27
|
-
gem.add_development_dependency "bundler"
|
28
|
-
gem.add_development_dependency "pry"
|
29
|
-
gem.add_development_dependency "railties", ">= 3.0.0"
|
30
|
-
gem.add_development_dependency "rake"
|
31
|
-
gem.add_development_dependency "rspec", ">= 3.0.0"
|
32
|
-
gem.add_development_dependency "rubocop"
|
33
|
-
gem.add_development_dependency "simplecov", ">= 0.17.0"
|
34
|
-
gem.add_development_dependency "yard"
|
35
31
|
end
|
data/spec/authorization_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "spec_helper"
|
4
|
+
require "action_controller/metal/strong_parameters"
|
4
5
|
|
5
6
|
describe Pundit::Authorization do
|
6
7
|
def to_params(*args, **kwargs, &block)
|
@@ -8,7 +9,7 @@ describe Pundit::Authorization do
|
|
8
9
|
end
|
9
10
|
|
10
11
|
let(:controller) { Controller.new(user, "update", to_params({})) }
|
11
|
-
let(:user) { double }
|
12
|
+
let(:user) { double("user") }
|
12
13
|
let(:post) { Post.new(user) }
|
13
14
|
let(:comment) { Comment.new }
|
14
15
|
let(:article) { Article.new }
|
@@ -157,7 +158,7 @@ describe Pundit::Authorization do
|
|
157
158
|
end
|
158
159
|
|
159
160
|
it "allows policy to be injected" do
|
160
|
-
new_policy =
|
161
|
+
new_policy = double
|
161
162
|
controller.policies[post] = new_policy
|
162
163
|
|
163
164
|
expect(controller.policy(post)).to eq new_policy
|
@@ -169,7 +170,7 @@ describe Pundit::Authorization do
|
|
169
170
|
expect(controller.policy_scope(Post)).to eq :published
|
170
171
|
end
|
171
172
|
|
172
|
-
it "allows policy scope class to be
|
173
|
+
it "allows policy scope class to be overridden" do
|
173
174
|
expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
|
174
175
|
end
|
175
176
|
|
@@ -182,7 +183,7 @@ describe Pundit::Authorization do
|
|
182
183
|
end
|
183
184
|
|
184
185
|
it "allows policy_scope to be injected" do
|
185
|
-
new_scope =
|
186
|
+
new_scope = double
|
186
187
|
controller.policy_scopes[Post] = new_scope
|
187
188
|
|
188
189
|
expect(controller.policy_scope(Post)).to eq new_scope
|
@@ -271,4 +272,60 @@ describe Pundit::Authorization do
|
|
271
272
|
expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
|
272
273
|
end
|
273
274
|
end
|
275
|
+
|
276
|
+
describe "#pundit_reset!" do
|
277
|
+
it "allows authorize to react to a user change" do
|
278
|
+
expect(controller.authorize(post)).to be_truthy
|
279
|
+
|
280
|
+
controller.current_user = double
|
281
|
+
controller.pundit_reset!
|
282
|
+
expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError)
|
283
|
+
end
|
284
|
+
|
285
|
+
it "allows policy to react to a user change" do
|
286
|
+
expect(controller.policy(DummyCurrentUser).user).to be user
|
287
|
+
|
288
|
+
new_user = double("new user")
|
289
|
+
controller.current_user = new_user
|
290
|
+
controller.pundit_reset!
|
291
|
+
expect(controller.policy(DummyCurrentUser).user).to be new_user
|
292
|
+
end
|
293
|
+
|
294
|
+
it "allows policy scope to react to a user change" do
|
295
|
+
expect(controller.policy_scope(DummyCurrentUser)).to be user
|
296
|
+
|
297
|
+
new_user = double("new user")
|
298
|
+
controller.current_user = new_user
|
299
|
+
controller.pundit_reset!
|
300
|
+
expect(controller.policy_scope(DummyCurrentUser)).to be new_user
|
301
|
+
end
|
302
|
+
|
303
|
+
it "resets the pundit context" do
|
304
|
+
expect(controller.pundit.user).to be(user)
|
305
|
+
|
306
|
+
new_user = double
|
307
|
+
controller.current_user = new_user
|
308
|
+
expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user)
|
309
|
+
end
|
310
|
+
|
311
|
+
it "clears pundit_policy_authorized? flag" do
|
312
|
+
expect(controller.pundit_policy_authorized?).to be false
|
313
|
+
|
314
|
+
controller.skip_authorization
|
315
|
+
expect(controller.pundit_policy_authorized?).to be true
|
316
|
+
|
317
|
+
controller.pundit_reset!
|
318
|
+
expect(controller.pundit_policy_authorized?).to be false
|
319
|
+
end
|
320
|
+
|
321
|
+
it "clears pundit_policy_scoped? flag" do
|
322
|
+
expect(controller.pundit_policy_scoped?).to be false
|
323
|
+
|
324
|
+
controller.skip_policy_scope
|
325
|
+
expect(controller.pundit_policy_scoped?).to be true
|
326
|
+
|
327
|
+
controller.pundit_reset!
|
328
|
+
expect(controller.pundit_policy_scoped?).to be false
|
329
|
+
end
|
330
|
+
end
|
274
331
|
end
|
@@ -18,5 +18,32 @@ RSpec.describe PostPolicy do
|
|
18
18
|
should permit(user, other_post)
|
19
19
|
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
20
20
|
end
|
21
|
+
|
22
|
+
it "uses the default description if not overridden" do
|
23
|
+
expect(permit(user, own_post).description).to eq("permit #{user.inspect} and #{own_post.inspect}")
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when the matcher description is overridden" do
|
27
|
+
after do
|
28
|
+
Pundit::RSpec::Matchers.description = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sets a custom matcher description with a Proc" do
|
32
|
+
allow(user).to receive(:role).and_return("default_role")
|
33
|
+
allow(own_post).to receive(:id).and_return(1)
|
34
|
+
|
35
|
+
Pundit::RSpec::Matchers.description = lambda { |user, record|
|
36
|
+
"permit user with role #{user.role} to access record with ID #{record.id}"
|
37
|
+
}
|
38
|
+
|
39
|
+
description = permit(user, own_post).description
|
40
|
+
expect(description).to eq("permit user with role default_role to access record with ID 1")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "sets a custom matcher description with a string" do
|
44
|
+
Pundit::RSpec::Matchers.description = "permit user"
|
45
|
+
expect(permit(user, own_post).description).to eq("permit user")
|
46
|
+
end
|
47
|
+
end
|
21
48
|
end
|
22
49
|
end
|