pundit 2.4.0 → 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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +92 -57
  3. data/.rubocop.yml +18 -8
  4. data/.rubocop_ignore_git.yml +7 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +61 -42
  7. data/Gemfile +22 -2
  8. data/README.md +30 -0
  9. data/Rakefile +1 -0
  10. data/lib/generators/pundit/install/install_generator.rb +3 -1
  11. data/lib/generators/pundit/policy/policy_generator.rb +3 -1
  12. data/lib/generators/rspec/policy_generator.rb +4 -1
  13. data/lib/generators/test_unit/policy_generator.rb +4 -1
  14. data/lib/pundit/authorization.rb +152 -77
  15. data/lib/pundit/cache_store/legacy_store.rb +7 -0
  16. data/lib/pundit/cache_store/null_store.rb +9 -0
  17. data/lib/pundit/cache_store.rb +22 -0
  18. data/lib/pundit/context.rb +76 -26
  19. data/lib/pundit/policy_finder.rb +22 -1
  20. data/lib/pundit/railtie.rb +19 -0
  21. data/lib/pundit/rspec.rb +67 -6
  22. data/lib/pundit/version.rb +2 -1
  23. data/lib/pundit.rb +39 -14
  24. data/pundit.gemspec +8 -12
  25. data/spec/authorization_spec.rb +60 -3
  26. data/spec/policy_finder_spec.rb +5 -1
  27. data/spec/pundit/helper_spec.rb +18 -0
  28. data/spec/pundit_spec.rb +37 -11
  29. data/spec/rspec_dsl_spec.rb +81 -0
  30. data/spec/simple_cov_check_action_formatter.rb +79 -0
  31. data/spec/spec_helper.rb +22 -339
  32. data/spec/support/lib/controller.rb +38 -0
  33. data/spec/support/lib/custom_cache.rb +19 -0
  34. data/spec/support/lib/instance_tracking.rb +20 -0
  35. data/spec/support/models/article.rb +4 -0
  36. data/spec/support/models/article_tag.rb +7 -0
  37. data/spec/support/models/artificial_blog.rb +7 -0
  38. data/spec/support/models/blog.rb +4 -0
  39. data/spec/support/models/comment.rb +5 -0
  40. data/spec/support/models/comment_four_five_six.rb +5 -0
  41. data/spec/support/models/comment_scope.rb +13 -0
  42. data/spec/support/models/comments_relation.rb +15 -0
  43. data/spec/support/models/customer/post.rb +11 -0
  44. data/spec/support/models/default_scope_contains_error.rb +5 -0
  45. data/spec/support/models/dummy_current_user.rb +7 -0
  46. data/spec/support/models/foo.rb +4 -0
  47. data/spec/support/models/post.rb +25 -0
  48. data/spec/support/models/post_four_five_six.rb +9 -0
  49. data/spec/support/models/project_one_two_three/avatar_four_five_six.rb +7 -0
  50. data/spec/support/models/project_one_two_three/tag_four_five_six.rb +11 -0
  51. data/spec/support/models/wiki.rb +4 -0
  52. data/spec/support/policies/article_tag_other_name_policy.rb +13 -0
  53. data/spec/support/policies/base_policy.rb +23 -0
  54. data/spec/support/policies/blog_policy.rb +5 -0
  55. data/spec/support/policies/comment_policy.rb +11 -0
  56. data/spec/support/policies/criteria_policy.rb +5 -0
  57. data/spec/support/policies/default_scope_contains_error_policy.rb +10 -0
  58. data/spec/support/policies/denier_policy.rb +7 -0
  59. data/spec/support/policies/dummy_current_user_policy.rb +9 -0
  60. data/spec/support/policies/nil_class_policy.rb +17 -0
  61. data/spec/support/policies/post_policy.rb +36 -0
  62. data/spec/support/policies/project/admin/comment_policy.rb +15 -0
  63. data/spec/support/policies/project/comment_policy.rb +17 -0
  64. data/spec/support/policies/project/criteria_policy.rb +7 -0
  65. data/spec/support/policies/project/post_policy.rb +13 -0
  66. data/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb +6 -0
  67. data/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb +6 -0
  68. data/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb +6 -0
  69. data/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb +6 -0
  70. data/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb +6 -0
  71. data/spec/support/policies/publication_policy.rb +13 -0
  72. data/spec/support/policies/wiki_policy.rb +8 -0
  73. metadata +62 -158
  74. data/spec/dsl_spec.rb +0 -30
  75. /data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  76. /data/lib/generators/pundit/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  77. /data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  78. /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
+ # Pundit DSL to include in your controllers to provide authorization helpers.
5
+ #
6
+ # @example
7
+ # class ApplicationController < ActionController::Base
8
+ # include Pundit::Authorization
9
+ # end
10
+ # @see #pundit
11
+ # @api public
4
12
  module Authorization
5
13
  extend ActiveSupport::Concern
6
14
 
@@ -15,7 +23,13 @@ module Pundit
15
23
 
16
24
  protected
17
25
 
18
- # @return [Pundit::Context] a new instance of {Pundit::Context} with the current user
26
+ # An instance of {Pundit::Context} initialized with the current user.
27
+ #
28
+ # @note this method is memoized and will return the same instance during the request.
29
+ # @api public
30
+ # @return [Pundit::Context]
31
+ # @see #pundit_user
32
+ # @see #policies
19
33
  def pundit
20
34
  @pundit ||= Pundit::Context.new(
21
35
  user: pundit_user,
@@ -23,40 +37,38 @@ module Pundit
23
37
  )
24
38
  end
25
39
 
26
- # @return [Boolean] whether authorization has been performed, i.e. whether
27
- # one {#authorize} or {#skip_authorization} has been called
28
- def pundit_policy_authorized?
29
- !!@_pundit_policy_authorized
30
- end
31
-
32
- # @return [Boolean] whether policy scoping has been performed, i.e. whether
33
- # one {#policy_scope} or {#skip_policy_scope} has been called
34
- def pundit_policy_scoped?
35
- !!@_pundit_policy_scoped
36
- end
37
-
38
- # Raises an error if authorization has not been performed, usually used as an
39
- # `after_action` filter to prevent programmer error in forgetting to call
40
- # {#authorize} or {#skip_authorization}.
40
+ # Hook method which allows customizing which user is passed to policies and
41
+ # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
41
42
  #
42
- # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
43
- # @raise [AuthorizationNotPerformedError] if authorization has not been performed
44
- # @return [void]
45
- def verify_authorized
46
- raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
43
+ # @note Make sure to call `pundit_reset!` if this changes during a request.
44
+ # @see https://github.com/varvet/pundit#customize-pundit-user
45
+ # @see #pundit
46
+ # @see #pundit_reset!
47
+ # @return [Object] the user object to be used with pundit
48
+ def pundit_user
49
+ current_user
47
50
  end
48
51
 
49
- # Raises an error if policy scoping has not been performed, usually used as an
50
- # `after_action` filter to prevent programmer error in forgetting to call
51
- # {#policy_scope} or {#skip_policy_scope} in index actions.
52
+ # Clears the cached Pundit authorization data.
53
+ #
54
+ # This method should be called when the pundit_user is changed,
55
+ # such as during user switching, to ensure that stale authorization
56
+ # data is not used. Pundit caches authorization policies and scopes
57
+ # for the pundit_user, so calling this method will reset those
58
+ # caches and ensure that the next authorization checks are performed
59
+ # with the correct context for the new pundit_user.
52
60
  #
53
- # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
54
- # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
55
61
  # @return [void]
56
- def verify_policy_scoped
57
- raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
62
+ def pundit_reset!
63
+ @pundit = nil
64
+ @_pundit_policies = nil
65
+ @_pundit_policy_scopes = nil
66
+ @_pundit_policy_authorized = nil
67
+ @_pundit_policy_scoped = nil
58
68
  end
59
69
 
70
+ # @!group Policies
71
+
60
72
  # Retrieves the policy for the given record, initializing it with the record
61
73
  # and current user and finally throwing an error if the user is not
62
74
  # authorized to perform the given action.
@@ -66,7 +78,9 @@ module Pundit
66
78
  # If omitted then this defaults to the Rails controller action name.
67
79
  # @param policy_class [Class] the policy class we want to force use of
68
80
  # @raise [NotAuthorizedError] if the given query method returned false
69
- # @return [Object] Always returns the passed object record
81
+ # @return [record] Always returns the passed object record
82
+ # @see Pundit::Context#authorize
83
+ # @see #verify_authorized
70
84
  def authorize(record, query = nil, policy_class: nil)
71
85
  query ||= "#{action_name}?"
72
86
 
@@ -79,29 +93,45 @@ module Pundit
79
93
  #
80
94
  # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
81
95
  # @return [void]
96
+ # @see #verify_authorized
82
97
  def skip_authorization
83
98
  @_pundit_policy_authorized = :skipped
84
99
  end
85
100
 
86
- # Allow this action not to perform policy scoping.
101
+ # @return [Boolean] wether or not authorization has been performed
102
+ # @see #authorize
103
+ # @see #skip_authorization
104
+ def pundit_policy_authorized?
105
+ !!@_pundit_policy_authorized
106
+ end
107
+
108
+ # Raises an error if authorization has not been performed.
109
+ #
110
+ # Usually used as an `after_action` filter to prevent programmer error in
111
+ # forgetting to call {#authorize} or {#skip_authorization}.
87
112
  #
88
113
  # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
114
+ # @raise [AuthorizationNotPerformedError] if authorization has not been performed
89
115
  # @return [void]
90
- def skip_policy_scope
91
- @_pundit_policy_scoped = :skipped
116
+ # @see #authorize
117
+ # @see #skip_authorization
118
+ def verify_authorized
119
+ raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
92
120
  end
93
121
 
94
- # Retrieves the policy scope for the given record.
122
+ # rubocop:disable Naming/MemoizedInstanceVariableName
123
+
124
+ # Cache of policies. You should not rely on this method.
95
125
  #
96
- # @see https://github.com/varvet/pundit#scopes
97
- # @param scope [Object] the object we're retrieving the policy scope for
98
- # @param policy_scope_class [Class] the policy scope class we want to force use of
99
- # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
100
- def policy_scope(scope, policy_scope_class: nil)
101
- @_pundit_policy_scoped = true
102
- policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
126
+ # @api private
127
+ def policies
128
+ @_pundit_policies ||= {}
103
129
  end
104
130
 
131
+ # rubocop:enable Naming/MemoizedInstanceVariableName
132
+
133
+ # @!endgroup
134
+
105
135
  # Retrieves the policy for the given record.
106
136
  #
107
137
  # @see https://github.com/varvet/pundit#policies
@@ -111,11 +141,87 @@ module Pundit
111
141
  pundit.policy!(record)
112
142
  end
113
143
 
114
- # Retrieves a set of permitted attributes from the policy by instantiating
115
- # the policy class for the given record and calling `permitted_attributes` on
116
- # it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers
117
- # what key the record should have in the params hash and retrieves the
118
- # permitted attributes from the params hash under that key.
144
+ # @!group Policy Scopes
145
+
146
+ # Retrieves the policy scope for the given record.
147
+ #
148
+ # @see https://github.com/varvet/pundit#scopes
149
+ # @param scope [Object] the object we're retrieving the policy scope for
150
+ # @param policy_scope_class [#resolve] the policy scope class we want to force use of
151
+ # @return [#resolve, nil] instance of scope class which can resolve to a scope
152
+ def policy_scope(scope, policy_scope_class: nil)
153
+ @_pundit_policy_scoped = true
154
+ policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
155
+ end
156
+
157
+ # Allow this action not to perform policy scoping.
158
+ #
159
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
160
+ # @return [void]
161
+ # @see #verify_policy_scoped
162
+ def skip_policy_scope
163
+ @_pundit_policy_scoped = :skipped
164
+ end
165
+
166
+ # @return [Boolean] wether or not policy scoping has been performed
167
+ # @see #policy_scope
168
+ # @see #skip_policy_scope
169
+ def pundit_policy_scoped?
170
+ !!@_pundit_policy_scoped
171
+ end
172
+
173
+ # Raises an error if policy scoping has not been performed.
174
+ #
175
+ # Usually used as an `after_action` filter to prevent programmer error in
176
+ # forgetting to call {#policy_scope} or {#skip_policy_scope} in index
177
+ # actions.
178
+ #
179
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
180
+ # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
181
+ # @return [void]
182
+ # @see #policy_scope
183
+ # @see #skip_policy_scope
184
+ def verify_policy_scoped
185
+ raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped?
186
+ end
187
+
188
+ # rubocop:disable Naming/MemoizedInstanceVariableName
189
+
190
+ # Cache of policy scope. You should not rely on this method.
191
+ #
192
+ # @api private
193
+ def policy_scopes
194
+ @_pundit_policy_scopes ||= {}
195
+ end
196
+
197
+ # rubocop:enable Naming/MemoizedInstanceVariableName
198
+
199
+ # This was added to allow calling `policy_scope!` without flipping the
200
+ # `pundit_policy_scoped?` flag.
201
+ #
202
+ # It's used internally by `policy_scope`, as well as from the views
203
+ # when they call `policy_scope`. It works because views get their helper
204
+ # from {Pundit::Helper}.
205
+ #
206
+ # @note This also memoizes the instance with `scope` as the key.
207
+ # @see Pundit::Helper#policy_scope
208
+ # @api private
209
+ def pundit_policy_scope(scope)
210
+ policy_scopes[scope] ||= pundit.policy_scope!(scope)
211
+ end
212
+ private :pundit_policy_scope
213
+
214
+ # @!endgroup
215
+
216
+ # @!group Strong Parameters
217
+
218
+ # Retrieves a set of permitted attributes from the policy.
219
+ #
220
+ # Done by instantiating the policy class for the given record and calling
221
+ # `permitted_attributes` on it, or `permitted_attributes_for_{action}` if
222
+ # `action` is defined. It then infers what key the record should have in the
223
+ # params hash and retrieves the permitted attributes from the params hash
224
+ # under that key.
119
225
  #
120
226
  # @see https://github.com/varvet/pundit#strong-parameters
121
227
  # @param record [Object] the object we're retrieving permitted attributes for
@@ -140,37 +246,6 @@ module Pundit
140
246
  params.require(PolicyFinder.new(record).param_key)
141
247
  end
142
248
 
143
- # Cache of policies. You should not rely on this method.
144
- #
145
- # @api private
146
- # rubocop:disable Naming/MemoizedInstanceVariableName
147
- def policies
148
- @_pundit_policies ||= {}
149
- end
150
- # rubocop:enable Naming/MemoizedInstanceVariableName
151
-
152
- # Cache of policy scope. You should not rely on this method.
153
- #
154
- # @api private
155
- # rubocop:disable Naming/MemoizedInstanceVariableName
156
- def policy_scopes
157
- @_pundit_policy_scopes ||= {}
158
- end
159
- # rubocop:enable Naming/MemoizedInstanceVariableName
160
-
161
- # Hook method which allows customizing which user is passed to policies and
162
- # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
163
- #
164
- # @see https://github.com/varvet/pundit#customize-pundit-user
165
- # @return [Object] the user object to be used with pundit
166
- def pundit_user
167
- current_user
168
- end
169
-
170
- private
171
-
172
- def pundit_policy_scope(scope)
173
- policy_scopes[scope] ||= pundit.policy_scope!(scope)
174
- end
249
+ # @!endgroup
175
250
  end
176
251
  end
@@ -2,12 +2,19 @@
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
6
10
  class LegacyStore
7
11
  def initialize(hash = {})
8
12
  @store = hash
9
13
  end
10
14
 
15
+ # A cache store that uses only the record as a cache key, and ignores the user.
16
+ #
17
+ # @note `nil` results are not cached.
11
18
  def fetch(user:, record:)
12
19
  _ = user
13
20
  @store[record] ||= yield
@@ -2,14 +2,23 @@
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
6
11
  class NullStore
7
12
  @instance = new
8
13
 
9
14
  class << self
15
+ # @return [NullStore] the singleton instance
10
16
  attr_reader :instance
11
17
  end
12
18
 
19
+ # Always yields, does not cache anything.
20
+ # @yield
21
+ # @return [any] whatever the block returns.
13
22
  def fetch(*, **)
14
23
  yield
15
24
  end
@@ -0,0 +1,22 @@
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
+ module CacheStore
9
+ # @!group Cache Store Interface
10
+
11
+ # @!method fetch(user:, record:, &block)
12
+ # Looks up a stored policy or generate a new one.
13
+ #
14
+ # @note This is a method template, but the method does not exist in this module.
15
+ # @param user [Object] the user that initiated the action
16
+ # @param record [Object] the object being accessed
17
+ # @param block [Proc] the block to execute if missing
18
+ # @return [Object] the policy
19
+
20
+ # @!endgroup
21
+ end
22
+ end
@@ -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
- # Retrieves the policy for the given record.
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
@@ -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