pundit 2.1.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +26 -0
  4. data/.github/PULL_REQUEST_TEMPLATE/gem_release_template.md +8 -0
  5. data/.github/pull_request_template.md +9 -0
  6. data/.github/workflows/main.yml +147 -0
  7. data/.github/workflows/push_gem.yml +33 -0
  8. data/.gitignore +1 -0
  9. data/.rubocop.yml +26 -29
  10. data/.rubocop_ignore_git.yml +7 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +120 -21
  13. data/CODE_OF_CONDUCT.md +1 -1
  14. data/CONTRIBUTING.md +3 -5
  15. data/Gemfile +23 -2
  16. data/README.md +175 -78
  17. data/Rakefile +1 -0
  18. data/SECURITY.md +19 -0
  19. data/config/rubocop-rspec.yml +5 -0
  20. data/lib/generators/pundit/install/install_generator.rb +6 -2
  21. data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +7 -3
  22. data/lib/generators/pundit/policy/policy_generator.rb +6 -2
  23. data/lib/generators/pundit/policy/templates/policy.rb.tt +16 -0
  24. data/lib/generators/rspec/policy_generator.rb +7 -2
  25. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +1 -1
  26. data/lib/generators/test_unit/policy_generator.rb +7 -2
  27. data/lib/pundit/authorization.rb +251 -0
  28. data/lib/pundit/cache_store/legacy_store.rb +24 -0
  29. data/lib/pundit/cache_store/null_store.rb +27 -0
  30. data/lib/pundit/cache_store.rb +22 -0
  31. data/lib/pundit/context.rb +177 -0
  32. data/lib/pundit/policy_finder.rb +24 -3
  33. data/lib/pundit/railtie.rb +19 -0
  34. data/lib/pundit/rspec.rb +93 -20
  35. data/lib/pundit/version.rb +2 -1
  36. data/lib/pundit.rb +68 -257
  37. data/pundit.gemspec +10 -10
  38. data/spec/authorization_spec.rb +331 -0
  39. data/spec/generators_spec.rb +43 -0
  40. data/spec/policies/post_policy_spec.rb +28 -1
  41. data/spec/policy_finder_spec.rb +84 -17
  42. data/spec/pundit/helper_spec.rb +18 -0
  43. data/spec/pundit_spec.rb +110 -233
  44. data/spec/rspec_dsl_spec.rb +81 -0
  45. data/spec/simple_cov_check_action_formatter.rb +79 -0
  46. data/spec/spec_helper.rb +29 -265
  47. data/spec/support/lib/controller.rb +38 -0
  48. data/spec/support/lib/custom_cache.rb +19 -0
  49. data/spec/support/lib/instance_tracking.rb +20 -0
  50. data/spec/support/models/article.rb +4 -0
  51. data/spec/support/models/article_tag.rb +7 -0
  52. data/spec/support/models/artificial_blog.rb +7 -0
  53. data/spec/support/models/blog.rb +4 -0
  54. data/spec/support/models/comment.rb +5 -0
  55. data/spec/support/models/comment_four_five_six.rb +5 -0
  56. data/spec/support/models/comment_scope.rb +13 -0
  57. data/spec/support/models/comments_relation.rb +15 -0
  58. data/spec/support/models/customer/post.rb +11 -0
  59. data/spec/support/models/default_scope_contains_error.rb +5 -0
  60. data/spec/support/models/dummy_current_user.rb +7 -0
  61. data/spec/support/models/foo.rb +4 -0
  62. data/spec/support/models/post.rb +25 -0
  63. data/spec/support/models/post_four_five_six.rb +9 -0
  64. data/spec/support/models/project_one_two_three/avatar_four_five_six.rb +7 -0
  65. data/spec/support/models/project_one_two_three/tag_four_five_six.rb +11 -0
  66. data/spec/support/models/wiki.rb +4 -0
  67. data/spec/support/policies/article_tag_other_name_policy.rb +13 -0
  68. data/spec/support/policies/base_policy.rb +23 -0
  69. data/spec/support/policies/blog_policy.rb +5 -0
  70. data/spec/support/policies/comment_policy.rb +11 -0
  71. data/spec/support/policies/criteria_policy.rb +5 -0
  72. data/spec/support/policies/default_scope_contains_error_policy.rb +10 -0
  73. data/spec/support/policies/denier_policy.rb +7 -0
  74. data/spec/support/policies/dummy_current_user_policy.rb +9 -0
  75. data/spec/support/policies/nil_class_policy.rb +17 -0
  76. data/spec/support/policies/post_policy.rb +36 -0
  77. data/spec/support/policies/project/admin/comment_policy.rb +15 -0
  78. data/spec/support/policies/project/comment_policy.rb +17 -0
  79. data/spec/support/policies/project/criteria_policy.rb +7 -0
  80. data/spec/support/policies/project/post_policy.rb +13 -0
  81. data/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb +6 -0
  82. data/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb +6 -0
  83. data/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb +6 -0
  84. data/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb +6 -0
  85. data/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb +6 -0
  86. data/spec/support/policies/publication_policy.rb +13 -0
  87. data/spec/support/policies/wiki_policy.rb +8 -0
  88. metadata +80 -130
  89. data/.travis.yml +0 -21
  90. data/lib/generators/pundit/policy/templates/policy.rb +0 -9
  91. /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
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
12
+ module Authorization
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ helper Helper if respond_to?(:helper)
17
+ if respond_to?(:helper_method)
18
+ helper_method :policy
19
+ helper_method :pundit_policy_scope
20
+ helper_method :pundit_user
21
+ end
22
+ end
23
+
24
+ protected
25
+
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
33
+ def pundit
34
+ @pundit ||= Pundit::Context.new(
35
+ user: pundit_user,
36
+ policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
37
+ )
38
+ end
39
+
40
+ # Hook method which allows customizing which user is passed to policies and
41
+ # scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
42
+ #
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
50
+ end
51
+
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.
60
+ #
61
+ # @return [void]
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
68
+ end
69
+
70
+ # @!group Policies
71
+
72
+ # Retrieves the policy for the given record, initializing it with the record
73
+ # and current user and finally throwing an error if the user is not
74
+ # authorized to perform the given action.
75
+ #
76
+ # @param record [Object, Array] the object we're checking permissions of
77
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
78
+ # If omitted then this defaults to the Rails controller action name.
79
+ # @param policy_class [Class] the policy class we want to force use of
80
+ # @raise [NotAuthorizedError] if the given query method returned false
81
+ # @return [record] Always returns the passed object record
82
+ # @see Pundit::Context#authorize
83
+ # @see #verify_authorized
84
+ def authorize(record, query = nil, policy_class: nil)
85
+ query ||= "#{action_name}?"
86
+
87
+ @_pundit_policy_authorized = true
88
+
89
+ pundit.authorize(record, query: query, policy_class: policy_class)
90
+ end
91
+
92
+ # Allow this action not to perform authorization.
93
+ #
94
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
95
+ # @return [void]
96
+ # @see #verify_authorized
97
+ def skip_authorization
98
+ @_pundit_policy_authorized = :skipped
99
+ end
100
+
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}.
112
+ #
113
+ # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
114
+ # @raise [AuthorizationNotPerformedError] if authorization has not been performed
115
+ # @return [void]
116
+ # @see #authorize
117
+ # @see #skip_authorization
118
+ def verify_authorized
119
+ raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized?
120
+ end
121
+
122
+ # rubocop:disable Naming/MemoizedInstanceVariableName
123
+
124
+ # Cache of policies. You should not rely on this method.
125
+ #
126
+ # @api private
127
+ def policies
128
+ @_pundit_policies ||= {}
129
+ end
130
+
131
+ # rubocop:enable Naming/MemoizedInstanceVariableName
132
+
133
+ # @!endgroup
134
+
135
+ # Retrieves the policy for the given record.
136
+ #
137
+ # @see https://github.com/varvet/pundit#policies
138
+ # @param record [Object] the object we're retrieving the policy for
139
+ # @return [Object] instance of policy class with query methods
140
+ def policy(record)
141
+ pundit.policy!(record)
142
+ end
143
+
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.
225
+ #
226
+ # @see https://github.com/varvet/pundit#strong-parameters
227
+ # @param record [Object] the object we're retrieving permitted attributes for
228
+ # @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
229
+ # If omitted then this defaults to the Rails controller action name.
230
+ # @return [Hash{String => Object}] the permitted attributes
231
+ def permitted_attributes(record, action = action_name)
232
+ policy = policy(record)
233
+ method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
234
+ "permitted_attributes_for_#{action}"
235
+ else
236
+ "permitted_attributes"
237
+ end
238
+ pundit_params_for(record).permit(*policy.public_send(method_name))
239
+ end
240
+
241
+ # Retrieves the params for the given record.
242
+ #
243
+ # @param record [Object] the object we're retrieving params for
244
+ # @return [ActionController::Parameters] the params
245
+ def pundit_params_for(record)
246
+ params.require(PolicyFinder.new(record).param_key)
247
+ end
248
+
249
+ # @!endgroup
250
+ end
251
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
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
+ #
9
+ # @api private
10
+ class LegacyStore
11
+ def initialize(hash = {})
12
+ @store = hash
13
+ end
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.
18
+ def fetch(user:, record:)
19
+ _ = user
20
+ @store[record] ||= yield
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
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
10
+ # @api private
11
+ class NullStore
12
+ @instance = new
13
+
14
+ class << self
15
+ # @return [NullStore] the singleton instance
16
+ attr_reader :instance
17
+ end
18
+
19
+ # Always yields, does not cache anything.
20
+ # @yield
21
+ # @return [any] whatever the block returns.
22
+ def fetch(*, **)
23
+ yield
24
+ end
25
+ end
26
+ end
27
+ 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
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
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
+ 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})
32
+ def initialize(user:, policy_cache: CacheStore::NullStore.instance)
33
+ @user = user
34
+ @policy_cache = policy_cache
35
+ end
36
+
37
+ # @api public
38
+ # @see #initialize
39
+ attr_reader :user
40
+
41
+ # @api private
42
+ # @see #initialize
43
+ attr_reader :policy_cache
44
+
45
+ # @!group Policies
46
+
47
+ # Retrieves the policy for the given record, initializing it with the
48
+ # record and user and finally throwing an error if the user is not
49
+ # authorized to perform the given action.
50
+ #
51
+ # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
52
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
53
+ # @param policy_class [Class] the policy class we want to force use of
54
+ # @raise [NotAuthorizedError] if the given query method returned false
55
+ # @return [Object] Always returns the passed object record
56
+ def authorize(possibly_namespaced_record, query:, policy_class:)
57
+ record = pundit_model(possibly_namespaced_record)
58
+ policy = if policy_class
59
+ policy_class.new(user, record)
60
+ else
61
+ policy!(possibly_namespaced_record)
62
+ end
63
+
64
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
65
+
66
+ record
67
+ end
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
+
94
+ # Retrieves the policy scope for the given record.
95
+ #
96
+ # @see https://github.com/varvet/pundit#scopes
97
+ # @param scope [Object] the object we're retrieving the policy scope for
98
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
99
+ # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
100
+ def policy_scope(scope)
101
+ policy_scope_class = policy_finder(scope).scope
102
+ return unless policy_scope_class
103
+
104
+ begin
105
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
106
+ rescue ArgumentError
107
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
108
+ end
109
+
110
+ policy_scope.resolve
111
+ end
112
+
113
+ # Retrieves the policy scope for the given record. Raises if not found.
114
+ #
115
+ # @see https://github.com/varvet/pundit#scopes
116
+ # @param scope [Object] the object we're retrieving the policy scope for
117
+ # @raise [NotDefinedError] if the policy scope cannot be found
118
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
119
+ # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
120
+ def policy_scope!(scope)
121
+ policy_scope_class = policy_finder(scope).scope!
122
+
123
+ begin
124
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
125
+ rescue ArgumentError
126
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
127
+ end
128
+
129
+ policy_scope.resolve
130
+ end
131
+
132
+ # @!endgroup
133
+
134
+ private
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
147
+ def cached_find(record)
148
+ policy_cache.fetch(user: user, record: record) do
149
+ klass = yield policy_finder(record)
150
+ next unless klass
151
+
152
+ model = pundit_model(record)
153
+
154
+ begin
155
+ klass.new(user, model)
156
+ rescue ArgumentError
157
+ raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
158
+ end
159
+ end
160
+ end
161
+
162
+ # Return a policy finder for the given record.
163
+ #
164
+ # @api private
165
+ # @return [PolicyFinder]
166
+ def policy_finder(record)
167
+ PolicyFinder.new(record)
168
+ end
169
+
170
+ # Given a possibly namespaced record, return the actual record.
171
+ #
172
+ # @api private
173
+ def pundit_model(record)
174
+ record.is_a?(Array) ? record.last : record
175
+ end
176
+ end
177
+ 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
@@ -56,7 +64,7 @@ module Pundit
56
64
 
57
65
  # @return [String] the name of the key this object would have in a params hash
58
66
  #
59
- def param_key
67
+ def param_key # rubocop:disable Metrics/AbcSize
60
68
  model = object.is_a?(Array) ? object.last : object
61
69
 
62
70
  if model.respond_to?(:model_name)
@@ -68,8 +76,13 @@ module Pundit
68
76
  end
69
77
  end
70
78
 
71
- private
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