pundit 2.4.0 → 2.5.1
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/CHANGELOG.md +66 -42
- data/README.md +31 -1
- 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 +170 -77
- data/lib/pundit/cache_store/legacy_store.rb +10 -0
- data/lib/pundit/cache_store/null_store.rb +12 -0
- data/lib/pundit/cache_store.rb +24 -0
- data/lib/pundit/context.rb +89 -26
- data/lib/pundit/error.rb +71 -0
- data/lib/pundit/helper.rb +16 -0
- data/lib/pundit/policy_finder.rb +33 -1
- data/lib/pundit/railtie.rb +20 -0
- data/lib/pundit/rspec.rb +69 -6
- data/lib/pundit/version.rb +2 -1
- data/lib/pundit.rb +27 -61
- metadata +19 -179
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -26
- data/.github/PULL_REQUEST_TEMPLATE/gem_release_template.md +0 -8
- data/.github/pull_request_template.md +0 -9
- data/.github/workflows/main.yml +0 -112
- data/.github/workflows/push_gem.yml +0 -33
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -63
- data/.yardopts +0 -1
- data/CODE_OF_CONDUCT.md +0 -28
- data/CONTRIBUTING.md +0 -31
- data/Gemfile +0 -8
- data/Rakefile +0 -20
- data/config/rubocop-rspec.yml +0 -5
- data/pundit.gemspec +0 -35
- data/spec/authorization_spec.rb +0 -274
- data/spec/dsl_spec.rb +0 -30
- data/spec/generators_spec.rb +0 -43
- data/spec/policies/post_policy_spec.rb +0 -49
- data/spec/policy_finder_spec.rb +0 -187
- data/spec/pundit_spec.rb +0 -448
- data/spec/spec_helper.rb +0 -352
- /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
@@ -2,12 +2,22 @@
|
|
2
2
|
|
3
3
|
module Pundit
|
4
4
|
module CacheStore
|
5
|
+
# A cache store that uses only the record as a cache key, and ignores the user.
|
6
|
+
#
|
7
|
+
# The original cache mechanism used by Pundit.
|
8
|
+
#
|
5
9
|
# @api private
|
10
|
+
# @since v2.3.2
|
6
11
|
class LegacyStore
|
12
|
+
# @since v2.3.2
|
7
13
|
def initialize(hash = {})
|
8
14
|
@store = hash
|
9
15
|
end
|
10
16
|
|
17
|
+
# A cache store that uses only the record as a cache key, and ignores the user.
|
18
|
+
#
|
19
|
+
# @note `nil` results are not cached.
|
20
|
+
# @since v2.3.2
|
11
21
|
def fetch(user:, record:)
|
12
22
|
_ = user
|
13
23
|
@store[record] ||= yield
|
@@ -2,14 +2,26 @@
|
|
2
2
|
|
3
3
|
module Pundit
|
4
4
|
module CacheStore
|
5
|
+
# A cache store that does not cache anything.
|
6
|
+
#
|
7
|
+
# Use `NullStore.instance` to get the singleton instance, it is thread-safe.
|
8
|
+
#
|
9
|
+
# @see Pundit::Context#initialize
|
5
10
|
# @api private
|
11
|
+
# @since v2.3.2
|
6
12
|
class NullStore
|
7
13
|
@instance = new
|
8
14
|
|
9
15
|
class << self
|
16
|
+
# @since v2.3.2
|
17
|
+
# @return [NullStore] the singleton instance
|
10
18
|
attr_reader :instance
|
11
19
|
end
|
12
20
|
|
21
|
+
# Always yields, does not cache anything.
|
22
|
+
# @yield
|
23
|
+
# @return [any] whatever the block returns.
|
24
|
+
# @since v2.3.2
|
13
25
|
def fetch(*, **)
|
14
26
|
yield
|
15
27
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pundit
|
4
|
+
# Namespace for cache store implementations.
|
5
|
+
#
|
6
|
+
# Cache stores are used to cache policy lookups, so you get the same policy
|
7
|
+
# instance for the same record.
|
8
|
+
# @since v2.3.2
|
9
|
+
module CacheStore
|
10
|
+
# @!group Cache Store Interface
|
11
|
+
|
12
|
+
# @!method fetch(user:, record:, &block)
|
13
|
+
# Looks up a stored policy or generate a new one.
|
14
|
+
#
|
15
|
+
# @since v2.3.2
|
16
|
+
# @note This is a method template, but the method does not exist in this module.
|
17
|
+
# @param user [Object] the user that initiated the action
|
18
|
+
# @param record [Object] the object being accessed
|
19
|
+
# @param block [Proc] the block to execute if missing
|
20
|
+
# @return [Object] the policy
|
21
|
+
|
22
|
+
# @!endgroup
|
23
|
+
end
|
24
|
+
end
|
data/lib/pundit/context.rb
CHANGED
@@ -1,27 +1,64 @@
|
|
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
|
28
|
+
#
|
29
|
+
# @since v2.3.2
|
4
30
|
class Context
|
31
|
+
# @see Pundit::Authorization#pundit
|
32
|
+
# @param user later passed to policies and scopes
|
33
|
+
# @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore})
|
34
|
+
# @since v2.3.2
|
5
35
|
def initialize(user:, policy_cache: CacheStore::NullStore.instance)
|
6
36
|
@user = user
|
7
37
|
@policy_cache = policy_cache
|
8
38
|
end
|
9
39
|
|
40
|
+
# @api public
|
41
|
+
# @see #initialize
|
42
|
+
# @since v2.3.2
|
10
43
|
attr_reader :user
|
11
44
|
|
12
45
|
# @api private
|
46
|
+
# @see #initialize
|
47
|
+
# @since v2.3.2
|
13
48
|
attr_reader :policy_cache
|
14
49
|
|
50
|
+
# @!group Policies
|
51
|
+
|
15
52
|
# Retrieves the policy for the given record, initializing it with the
|
16
53
|
# record and user and finally throwing an error if the user is not
|
17
54
|
# authorized to perform the given action.
|
18
55
|
#
|
19
|
-
# @param user [Object] the user that initiated the action
|
20
56
|
# @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
|
21
57
|
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
|
22
58
|
# @param policy_class [Class] the policy class we want to force use of
|
23
59
|
# @raise [NotAuthorizedError] if the given query method returned false
|
24
60
|
# @return [Object] Always returns the passed object record
|
61
|
+
# @since v2.3.2
|
25
62
|
def authorize(possibly_namespaced_record, query:, policy_class:)
|
26
63
|
record = pundit_model(possibly_namespaced_record)
|
27
64
|
policy = if policy_class
|
@@ -35,13 +72,40 @@ module Pundit
|
|
35
72
|
record
|
36
73
|
end
|
37
74
|
|
75
|
+
# Retrieves the policy for the given record.
|
76
|
+
#
|
77
|
+
# @see https://github.com/varvet/pundit#policies
|
78
|
+
# @param record [Object] the object we're retrieving the policy for
|
79
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
80
|
+
# @return [Object, nil] instance of policy class with query methods
|
81
|
+
# @since v2.3.2
|
82
|
+
def policy(record)
|
83
|
+
cached_find(record, &:policy)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Retrieves the policy for the given record, or raises if not found.
|
87
|
+
#
|
88
|
+
# @see https://github.com/varvet/pundit#policies
|
89
|
+
# @param record [Object] the object we're retrieving the policy for
|
90
|
+
# @raise [NotDefinedError] if the policy cannot be found
|
91
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
92
|
+
# @return [Object] instance of policy class with query methods
|
93
|
+
# @since v2.3.2
|
94
|
+
def policy!(record)
|
95
|
+
cached_find(record, &:policy!)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @!endgroup
|
99
|
+
|
100
|
+
# @!group Scopes
|
101
|
+
|
38
102
|
# Retrieves the policy scope for the given record.
|
39
103
|
#
|
40
104
|
# @see https://github.com/varvet/pundit#scopes
|
41
|
-
# @param user [Object] the user that initiated the action
|
42
105
|
# @param scope [Object] the object we're retrieving the policy scope for
|
43
106
|
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
44
107
|
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
|
108
|
+
# @since v2.3.2
|
45
109
|
def policy_scope(scope)
|
46
110
|
policy_scope_class = policy_finder(scope).scope
|
47
111
|
return unless policy_scope_class
|
@@ -58,14 +122,13 @@ module Pundit
|
|
58
122
|
# Retrieves the policy scope for the given record. Raises if not found.
|
59
123
|
#
|
60
124
|
# @see https://github.com/varvet/pundit#scopes
|
61
|
-
# @param user [Object] the user that initiated the action
|
62
125
|
# @param scope [Object] the object we're retrieving the policy scope for
|
63
126
|
# @raise [NotDefinedError] if the policy scope cannot be found
|
64
127
|
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
65
128
|
# @return [Scope{#resolve}] instance of scope class which can resolve to a scope
|
129
|
+
# @since v2.3.2
|
66
130
|
def policy_scope!(scope)
|
67
131
|
policy_scope_class = policy_finder(scope).scope!
|
68
|
-
return unless policy_scope_class
|
69
132
|
|
70
133
|
begin
|
71
134
|
policy_scope = policy_scope_class.new(user, pundit_model(scope))
|
@@ -76,31 +139,22 @@ module Pundit
|
|
76
139
|
policy_scope.resolve
|
77
140
|
end
|
78
141
|
|
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
|
142
|
+
# @!endgroup
|
101
143
|
|
102
144
|
private
|
103
145
|
|
146
|
+
# @!group Private Helpers
|
147
|
+
|
148
|
+
# Finds a cached policy for the given record, or yields to find one.
|
149
|
+
#
|
150
|
+
# @api private
|
151
|
+
# @param record [Object] the object we're retrieving the policy for
|
152
|
+
# @yield a policy finder if no policy was cached
|
153
|
+
# @yieldparam [PolicyFinder] policy_finder
|
154
|
+
# @yieldreturn [#new(user, model)]
|
155
|
+
# @return [Policy, nil] an instantiated policy
|
156
|
+
# @raise [InvalidConstructorError] if policy can't be instantated
|
157
|
+
# @since v2.3.2
|
104
158
|
def cached_find(record)
|
105
159
|
policy_cache.fetch(user: user, record: record) do
|
106
160
|
klass = yield policy_finder(record)
|
@@ -116,10 +170,19 @@ module Pundit
|
|
116
170
|
end
|
117
171
|
end
|
118
172
|
|
173
|
+
# Return a policy finder for the given record.
|
174
|
+
#
|
175
|
+
# @api private
|
176
|
+
# @return [PolicyFinder]
|
177
|
+
# @since v2.3.2
|
119
178
|
def policy_finder(record)
|
120
179
|
PolicyFinder.new(record)
|
121
180
|
end
|
122
181
|
|
182
|
+
# Given a possibly namespaced record, return the actual record.
|
183
|
+
#
|
184
|
+
# @api private
|
185
|
+
# @since v2.3.2
|
123
186
|
def pundit_model(record)
|
124
187
|
record.is_a?(Array) ? record.last : record
|
125
188
|
end
|
data/lib/pundit/error.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pundit
|
4
|
+
# @api private
|
5
|
+
# @since v1.0.0
|
6
|
+
# To avoid name clashes with common Error naming when mixing in Pundit,
|
7
|
+
# keep it here with compact class style definition.
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
# Error that will be raised when authorization has failed
|
11
|
+
# @since v0.1.0
|
12
|
+
class NotAuthorizedError < Error
|
13
|
+
# @see #initialize
|
14
|
+
# @since v0.2.3
|
15
|
+
attr_reader :query
|
16
|
+
# @see #initialize
|
17
|
+
# @since v0.2.3
|
18
|
+
attr_reader :record
|
19
|
+
# @see #initialize
|
20
|
+
# @since v0.2.3
|
21
|
+
attr_reader :policy
|
22
|
+
|
23
|
+
# @since v1.0.0
|
24
|
+
#
|
25
|
+
# @overload initialize(message)
|
26
|
+
# Create an error with a simple error message.
|
27
|
+
# @param [String] message A simple error message string.
|
28
|
+
#
|
29
|
+
# @overload initialize(options)
|
30
|
+
# Create an error with the specified attributes.
|
31
|
+
# @param [Hash] options The error options.
|
32
|
+
# @option options [String] :message Optional custom error message. Will default to a generalized message.
|
33
|
+
# @option options [Symbol] :query The name of the policy method that was checked.
|
34
|
+
# @option options [Object] :record The object that was being checked with the policy.
|
35
|
+
# @option options [Class] :policy The class of policy that was used for the check.
|
36
|
+
def initialize(options = {})
|
37
|
+
if options.is_a? String
|
38
|
+
message = options
|
39
|
+
else
|
40
|
+
@query = options[:query]
|
41
|
+
@record = options[:record]
|
42
|
+
@policy = options[:policy]
|
43
|
+
|
44
|
+
message = options.fetch(:message) do
|
45
|
+
record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}"
|
46
|
+
"not allowed to #{policy.class}##{query} #{record_name}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
super(message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Error that will be raised if a policy or policy scope constructor is not called correctly.
|
55
|
+
# @since v2.0.0
|
56
|
+
class InvalidConstructorError < Error; end
|
57
|
+
|
58
|
+
# Error that will be raised if a controller action has not called the
|
59
|
+
# `authorize` or `skip_authorization` methods.
|
60
|
+
# @since v0.2.3
|
61
|
+
class AuthorizationNotPerformedError < Error; end
|
62
|
+
|
63
|
+
# Error that will be raised if a controller action has not called the
|
64
|
+
# `policy_scope` or `skip_policy_scope` methods.
|
65
|
+
# @since v0.3.0
|
66
|
+
class PolicyScopingNotPerformedError < AuthorizationNotPerformedError; end
|
67
|
+
|
68
|
+
# Error that will be raised if a policy or policy scope is not defined.
|
69
|
+
# @since v0.1.0
|
70
|
+
class NotDefinedError < Error; end
|
71
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pundit
|
4
|
+
# Rails view helpers, to allow a slightly different view-specific
|
5
|
+
# implementation of the methods in {Pundit::Authorization}.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
# @since v1.0.0
|
9
|
+
module Helper
|
10
|
+
# @see Pundit::Authorization#pundit_policy_scope
|
11
|
+
# @since v1.0.0
|
12
|
+
def policy_scope(scope)
|
13
|
+
pundit_policy_scope(scope)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/pundit/policy_finder.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
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.
|
8
|
+
# @since v0.1.0
|
5
9
|
# @api public
|
6
10
|
# @example
|
7
11
|
# user = User.find(params[:id])
|
@@ -10,10 +14,18 @@ module Pundit
|
|
10
14
|
# finder.scope #=> UserPolicy::Scope
|
11
15
|
#
|
12
16
|
class PolicyFinder
|
17
|
+
# A constant applied to the end of the class name to find the policy class.
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
# @since v2.5.0
|
21
|
+
SUFFIX = "Policy"
|
22
|
+
|
23
|
+
# @see #initialize
|
24
|
+
# @since v0.1.0
|
13
25
|
attr_reader :object
|
14
26
|
|
15
27
|
# @param object [any] the object to find policy and scope classes for
|
16
|
-
#
|
28
|
+
# @since v0.1.0
|
17
29
|
def initialize(object)
|
18
30
|
@object = object
|
19
31
|
end
|
@@ -24,6 +36,7 @@ module Pundit
|
|
24
36
|
# scope = finder.scope #=> UserPolicy::Scope
|
25
37
|
# scope.resolve #=> <#ActiveRecord::Relation ...>
|
26
38
|
#
|
39
|
+
# @since v0.1.0
|
27
40
|
def scope
|
28
41
|
"#{policy}::Scope".safe_constantize
|
29
42
|
end
|
@@ -35,6 +48,7 @@ module Pundit
|
|
35
48
|
# policy.show? #=> true
|
36
49
|
# policy.update? #=> false
|
37
50
|
#
|
51
|
+
# @since v0.1.0
|
38
52
|
def policy
|
39
53
|
klass = find(object)
|
40
54
|
klass.is_a?(String) ? klass.safe_constantize : klass
|
@@ -43,6 +57,7 @@ module Pundit
|
|
43
57
|
# @return [Scope{#resolve}] scope class which can resolve to a scope
|
44
58
|
# @raise [NotDefinedError] if scope could not be determined
|
45
59
|
#
|
60
|
+
# @since v0.1.0
|
46
61
|
def scope!
|
47
62
|
scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
|
48
63
|
end
|
@@ -50,12 +65,14 @@ module Pundit
|
|
50
65
|
# @return [Class] policy class with query methods
|
51
66
|
# @raise [NotDefinedError] if policy could not be determined
|
52
67
|
#
|
68
|
+
# @since v0.1.0
|
53
69
|
def policy!
|
54
70
|
policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
|
55
71
|
end
|
56
72
|
|
57
73
|
# @return [String] the name of the key this object would have in a params hash
|
58
74
|
#
|
75
|
+
# @since v1.1.0
|
59
76
|
def param_key # rubocop:disable Metrics/AbcSize
|
60
77
|
model = object.is_a?(Array) ? object.last : object
|
61
78
|
|
@@ -70,6 +87,12 @@ module Pundit
|
|
70
87
|
|
71
88
|
private
|
72
89
|
|
90
|
+
# Given an object, find the policy class name.
|
91
|
+
#
|
92
|
+
# Uses recursion to handle namespaces.
|
93
|
+
#
|
94
|
+
# @return [String, Class] the policy class, or its name.
|
95
|
+
# @since v0.2.0
|
73
96
|
def find(subject)
|
74
97
|
if subject.is_a?(Array)
|
75
98
|
modules = subject.dup
|
@@ -86,6 +109,15 @@ module Pundit
|
|
86
109
|
end
|
87
110
|
end
|
88
111
|
|
112
|
+
# Given an object, find its' class name.
|
113
|
+
#
|
114
|
+
# - Supports ActiveModel.
|
115
|
+
# - Supports regular classes.
|
116
|
+
# - Supports symbols.
|
117
|
+
# - Supports object instances.
|
118
|
+
#
|
119
|
+
# @return [String, Class] the class, or its name.
|
120
|
+
# @since v1.1.0
|
89
121
|
def find_class_name(subject)
|
90
122
|
if subject.respond_to?(:model_name)
|
91
123
|
subject.model_name
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pundit
|
4
|
+
# @since v2.5.0
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
if Rails.version.to_f >= 8.0
|
7
|
+
initializer "pundit.stats_directories" do
|
8
|
+
require "rails/code_statistics"
|
9
|
+
|
10
|
+
if Rails.root.join("app/policies").directory?
|
11
|
+
Rails::CodeStatistics.register_directory("Policies", "app/policies")
|
12
|
+
end
|
13
|
+
|
14
|
+
if Rails.root.join("test/policies").directory?
|
15
|
+
Rails::CodeStatistics.register_directory("Policy tests", "test/policies", test_directory: true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/pundit/rspec.rb
CHANGED
@@ -1,13 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "pundit"
|
4
|
+
# Array#to_sentence
|
5
|
+
require "active_support/core_ext/array/conversions"
|
6
|
+
|
3
7
|
module Pundit
|
8
|
+
# Namespace for Pundit's RSpec integration.
|
9
|
+
# @since v0.1.0
|
4
10
|
module RSpec
|
11
|
+
# Namespace for Pundit's RSpec matchers.
|
5
12
|
module Matchers
|
6
13
|
extend ::RSpec::Matchers::DSL
|
7
14
|
|
15
|
+
# @!method description=(description)
|
8
16
|
class << self
|
17
|
+
# Used to build a suitable description for the Pundit `permit` matcher.
|
18
|
+
# @api public
|
19
|
+
# @param value [String, Proc]
|
20
|
+
# @example
|
21
|
+
# Pundit::RSpec::Matchers.description = ->(user, record) do
|
22
|
+
# "permit user with role #{user.role} to access record with ID #{record.id}"
|
23
|
+
# end
|
9
24
|
attr_writer :description
|
10
25
|
|
26
|
+
# Used to retrieve a suitable description for the Pundit `permit` matcher.
|
27
|
+
# @api private
|
28
|
+
# @private
|
11
29
|
def description(user, record)
|
12
30
|
return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
|
13
31
|
|
@@ -32,15 +50,21 @@ module Pundit
|
|
32
50
|
end
|
33
51
|
|
34
52
|
failure_message_proc = lambda do |policy|
|
35
|
-
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
36
53
|
"Expected #{policy} to grant #{permissions.to_sentence} on " \
|
37
|
-
"#{record} but #{@violating_permissions.to_sentence} #{
|
54
|
+
"#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted"
|
38
55
|
end
|
39
56
|
|
40
57
|
failure_message_when_negated_proc = lambda do |policy|
|
41
|
-
was_were = @violating_permissions.count > 1 ? "were" : "was"
|
42
58
|
"Expected #{policy} not to grant #{permissions.to_sentence} on " \
|
43
|
-
"#{record} but #{@violating_permissions.to_sentence} #{
|
59
|
+
"#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted"
|
60
|
+
end
|
61
|
+
|
62
|
+
def was_or_were
|
63
|
+
if @violating_permissions.count > 1
|
64
|
+
"were"
|
65
|
+
else
|
66
|
+
"was"
|
67
|
+
end
|
44
68
|
end
|
45
69
|
|
46
70
|
description do
|
@@ -53,21 +77,57 @@ module Pundit
|
|
53
77
|
failure_message(&failure_message_proc)
|
54
78
|
failure_message_when_negated(&failure_message_when_negated_proc)
|
55
79
|
else
|
80
|
+
# :nocov:
|
81
|
+
# Compatibility with RSpec < 3.0, released 2014-06-01.
|
56
82
|
match_for_should(&match_proc)
|
57
83
|
match_for_should_not(&match_when_negated_proc)
|
58
84
|
failure_message_for_should(&failure_message_proc)
|
59
85
|
failure_message_for_should_not(&failure_message_when_negated_proc)
|
86
|
+
# :nocov:
|
87
|
+
end
|
88
|
+
|
89
|
+
if ::RSpec.respond_to?(:current_example)
|
90
|
+
def current_example
|
91
|
+
::RSpec.current_example
|
92
|
+
end
|
93
|
+
else
|
94
|
+
# :nocov:
|
95
|
+
# Compatibility with RSpec < 3.0, released 2014-06-01.
|
96
|
+
def current_example
|
97
|
+
example
|
98
|
+
end
|
99
|
+
# :nocov:
|
60
100
|
end
|
61
101
|
|
62
102
|
def permissions
|
63
|
-
current_example
|
64
|
-
|
103
|
+
current_example.metadata.fetch(:permissions) do
|
104
|
+
raise KeyError, <<~ERROR.strip
|
105
|
+
No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
|
106
|
+
ERROR
|
107
|
+
end
|
65
108
|
end
|
66
109
|
end
|
67
110
|
# rubocop:enable Metrics/BlockLength
|
68
111
|
end
|
69
112
|
|
113
|
+
# Mixed in to all policy example groups to provide a DSL.
|
70
114
|
module DSL
|
115
|
+
# @example
|
116
|
+
# describe PostPolicy do
|
117
|
+
# permissions :show?, :update? do
|
118
|
+
# it { is_expected.to permit(user, own_post) }
|
119
|
+
# end
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# @example focused example group
|
123
|
+
# describe PostPolicy do
|
124
|
+
# permissions :show?, :update?, :focus do
|
125
|
+
# it { is_expected.to permit(user, own_post) }
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# @param list [Symbol, Array<Symbol>] a permission to describe
|
130
|
+
# @return [void]
|
71
131
|
def permissions(*list, &block)
|
72
132
|
metadata = { permissions: list, caller: caller }
|
73
133
|
|
@@ -81,6 +141,9 @@ module Pundit
|
|
81
141
|
end
|
82
142
|
end
|
83
143
|
|
144
|
+
# Mixed in to all policy example groups.
|
145
|
+
#
|
146
|
+
# @private not useful
|
84
147
|
module PolicyExampleGroup
|
85
148
|
include Pundit::RSpec::Matchers
|
86
149
|
|