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.
Files changed (85) 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 +4 -4
  5. data/.github/workflows/main.yml +92 -52
  6. data/.github/workflows/push_gem.yml +4 -4
  7. data/.rubocop.yml +18 -8
  8. data/.rubocop_ignore_git.yml +7 -0
  9. data/.yardopts +1 -1
  10. data/CHANGELOG.md +68 -37
  11. data/CODE_OF_CONDUCT.md +1 -1
  12. data/CONTRIBUTING.md +1 -0
  13. data/Gemfile +22 -2
  14. data/README.md +88 -15
  15. data/Rakefile +1 -0
  16. data/lib/generators/pundit/install/install_generator.rb +3 -1
  17. data/lib/generators/pundit/policy/policy_generator.rb +3 -1
  18. data/lib/generators/rspec/policy_generator.rb +4 -1
  19. data/lib/generators/test_unit/policy_generator.rb +4 -1
  20. data/lib/pundit/authorization.rb +152 -77
  21. data/lib/pundit/cache_store/legacy_store.rb +7 -0
  22. data/lib/pundit/cache_store/null_store.rb +9 -0
  23. data/lib/pundit/cache_store.rb +22 -0
  24. data/lib/pundit/context.rb +76 -26
  25. data/lib/pundit/policy_finder.rb +22 -1
  26. data/lib/pundit/railtie.rb +19 -0
  27. data/lib/pundit/rspec.rb +90 -7
  28. data/lib/pundit/version.rb +2 -1
  29. data/lib/pundit.rb +43 -15
  30. data/pundit.gemspec +8 -12
  31. data/spec/authorization_spec.rb +61 -4
  32. data/spec/policies/post_policy_spec.rb +27 -0
  33. data/spec/policy_finder_spec.rb +5 -1
  34. data/spec/pundit/helper_spec.rb +18 -0
  35. data/spec/pundit_spec.rb +58 -15
  36. data/spec/rspec_dsl_spec.rb +81 -0
  37. data/spec/simple_cov_check_action_formatter.rb +79 -0
  38. data/spec/spec_helper.rb +22 -339
  39. data/spec/support/lib/controller.rb +38 -0
  40. data/spec/support/lib/custom_cache.rb +19 -0
  41. data/spec/support/lib/instance_tracking.rb +20 -0
  42. data/spec/support/models/article.rb +4 -0
  43. data/spec/support/models/article_tag.rb +7 -0
  44. data/spec/support/models/artificial_blog.rb +7 -0
  45. data/spec/support/models/blog.rb +4 -0
  46. data/spec/support/models/comment.rb +5 -0
  47. data/spec/support/models/comment_four_five_six.rb +5 -0
  48. data/spec/support/models/comment_scope.rb +13 -0
  49. data/spec/support/models/comments_relation.rb +15 -0
  50. data/spec/support/models/customer/post.rb +11 -0
  51. data/spec/support/models/default_scope_contains_error.rb +5 -0
  52. data/spec/support/models/dummy_current_user.rb +7 -0
  53. data/spec/support/models/foo.rb +4 -0
  54. data/spec/support/models/post.rb +25 -0
  55. data/spec/support/models/post_four_five_six.rb +9 -0
  56. data/spec/support/models/project_one_two_three/avatar_four_five_six.rb +7 -0
  57. data/spec/support/models/project_one_two_three/tag_four_five_six.rb +11 -0
  58. data/spec/support/models/wiki.rb +4 -0
  59. data/spec/support/policies/article_tag_other_name_policy.rb +13 -0
  60. data/spec/support/policies/base_policy.rb +23 -0
  61. data/spec/support/policies/blog_policy.rb +5 -0
  62. data/spec/support/policies/comment_policy.rb +11 -0
  63. data/spec/support/policies/criteria_policy.rb +5 -0
  64. data/spec/support/policies/default_scope_contains_error_policy.rb +10 -0
  65. data/spec/support/policies/denier_policy.rb +7 -0
  66. data/spec/support/policies/dummy_current_user_policy.rb +9 -0
  67. data/spec/support/policies/nil_class_policy.rb +17 -0
  68. data/spec/support/policies/post_policy.rb +36 -0
  69. data/spec/support/policies/project/admin/comment_policy.rb +15 -0
  70. data/spec/support/policies/project/comment_policy.rb +17 -0
  71. data/spec/support/policies/project/criteria_policy.rb +7 -0
  72. data/spec/support/policies/project/post_policy.rb +13 -0
  73. data/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb +6 -0
  74. data/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb +6 -0
  75. data/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb +6 -0
  76. data/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb +6 -0
  77. data/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb +6 -0
  78. data/spec/support/policies/publication_policy.rb +13 -0
  79. data/spec/support/policies/wiki_policy.rb +8 -0
  80. metadata +66 -158
  81. /data/.github/{PULL_REQUEST_TEMPLATE/pull_request_template.md → pull_request_template.md} +0 -0
  82. /data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  83. /data/lib/generators/pundit/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  84. /data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  85. /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![Main](https://github.com/varvet/pundit/actions/workflows/main.yml/badge.svg)](https://github.com/varvet/pundit/actions/workflows/main.yml)
4
4
  [![Code Climate](https://api.codeclimate.com/v1/badges/a940030f96c9fb43046a/maintainability)](https://codeclimate.com/github/varvet/pundit/maintainability)
5
- [![Inline docs](http://inch-ci.org/github/varvet/pundit.svg?branch=main)](http://inch-ci.org/github/varvet/pundit)
6
- [![Gem Version](https://badge.fury.io/rb/pundit.svg)](http://badge.fury.io/rb/pundit)
5
+ [![Inline docs](https://inch-ci.org/github/varvet/pundit.svg?branch=main)](https://inch-ci.org/github/varvet/pundit)
6
+ [![Gem Version](https://badge.fury.io/rb/pundit.svg)](https://badge.fury.io/rb/pundit)
7
7
 
8
8
  Pundit provides a set of helpers which guide you in leveraging regular Ruby
9
9
  classes and object oriented design patterns to build a straightforward, robust, and
@@ -11,7 +11,7 @@ scalable authorization system.
11
11
 
12
12
  ## Links:
13
13
 
14
- - [API documentation for the most recent version](http://www.rubydoc.info/gems/pundit)
14
+ - [API documentation for the most recent version](https://www.rubydoc.info/gems/pundit)
15
15
  - [Source Code](https://github.com/varvet/pundit)
16
16
  - [Contributing](https://github.com/varvet/pundit/blob/main/CONTRIBUTING.md)
17
17
  - [Code of Conduct](https://github.com/varvet/pundit/blob/main/CODE_OF_CONDUCT.md)
@@ -116,7 +116,7 @@ and the given record. It then infers from the action name, that it should call
116
116
 
117
117
  ``` ruby
118
118
  unless PostPolicy.new(current_user, @post).update?
119
- raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
119
+ raise Pundit::NotAuthorizedError, "not allowed to PostPolicy#update? this Post"
120
120
  end
121
121
  ```
122
122
 
@@ -360,8 +360,15 @@ authorize individual instances.
360
360
  ``` ruby
361
361
  class ApplicationController < ActionController::Base
362
362
  include Pundit::Authorization
363
- after_action :verify_authorized, except: :index
364
- after_action :verify_policy_scoped, only: :index
363
+ after_action :verify_pundit_authorization
364
+
365
+ def verify_pundit_authorization
366
+ if action_name == "index"
367
+ verify_policy_scoped
368
+ else
369
+ verify_authorized
370
+ end
371
+ end
365
372
  end
366
373
  ```
367
374
 
@@ -489,7 +496,7 @@ end
489
496
  ## Rescuing a denied Authorization in Rails
490
497
 
491
498
  Pundit raises a `Pundit::NotAuthorizedError` you can
492
- [rescue_from](http://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
499
+ [rescue_from](https://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
493
500
  in your `ApplicationController`. You can customize the `user_not_authorized`
494
501
  method in every controller.
495
502
 
@@ -503,7 +510,7 @@ class ApplicationController < ActionController::Base
503
510
 
504
511
  def user_not_authorized
505
512
  flash[:alert] = "You are not authorized to perform this action."
506
- redirect_back(fallback_location: root_path)
513
+ redirect_back_or_to(root_path)
507
514
  end
508
515
  end
509
516
  ```
@@ -532,7 +539,7 @@ class ApplicationController < ActionController::Base
532
539
  policy_name = exception.policy.class.to_s.underscore
533
540
 
534
541
  flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
535
- redirect_back(fallback_location: root_path)
542
+ redirect_back_or_to(root_path)
536
543
  end
537
544
  end
538
545
  ```
@@ -576,6 +583,36 @@ def pundit_user
576
583
  end
577
584
  ```
578
585
 
586
+ For instance, Rails 8 includes a built-in [authentication generator](https://github.com/rails/rails/tree/8-0-stable/railties/lib/rails/generators/rails/authentication). If you choose to use it, the currently logged-in user is accessed via `Current.user` instead of `current_user`.
587
+
588
+ To ensure compatibility with Pundit, define a `pundit_user` method in `application_controller.rb` (or another suitable location) as follows:
589
+
590
+ ```ruby
591
+ def pundit_user
592
+ Current.user
593
+ end
594
+ ```
595
+
596
+ ### Handling User Switching in Pundit
597
+
598
+ When switching users in your application, it's important to reset the Pundit user context to ensure that authorization policies are applied correctly for the new user. Pundit caches the user context, so failing to reset it could result in incorrect permissions being applied.
599
+
600
+ To handle user switching, you can use the following pattern in your controller:
601
+
602
+ ```ruby
603
+ class ApplicationController
604
+ include Pundit::Authorization
605
+
606
+ def switch_user_to(user)
607
+ terminate_session if authenticated?
608
+ start_new_session_for user
609
+ pundit_reset!
610
+ end
611
+ end
612
+ ```
613
+
614
+ Make sure to invoke `pundit_reset!` whenever changing the user. This ensures the cached authorization context is reset, preventing any incorrect permissions from being applied.
615
+
579
616
  ## Policy Namespacing
580
617
  In some cases it might be helpful to have multiple policies that serve different contexts for a
581
618
  resource. A prime example of this is the case where User policies differ from Admin policies. To
@@ -754,6 +791,10 @@ end
754
791
 
755
792
  ### Policy Specs
756
793
 
794
+ > [!TIP]
795
+ > An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
796
+ [excellent post](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/punditcommunity/pundit-matchers) gem.
797
+
757
798
  Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec.
758
799
  Require `pundit/rspec` in your `spec_helper.rb`:
759
800
 
@@ -783,8 +824,40 @@ describe PostPolicy do
783
824
  end
784
825
  ```
785
826
 
786
- An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
787
- [excellent post](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/punditcommunity/pundit-matchers) gem.
827
+ ### Custom matcher description
828
+
829
+ By default rspec includes an inspected `user` and `record` in the matcher description, which might become overly verbose:
830
+
831
+ ```
832
+ PostPolicy
833
+ update? and show?
834
+ is expected to permit #<User:0x0000000104aefd80> and #<Post:0x0000000104aef8d0 @user=#<User:0x0000000104aefd80>>
835
+ ```
836
+
837
+ You can override the default description with a static string, or a block:
838
+
839
+ ```ruby
840
+ # static alternative: Pundit::RSpec::Matchers.description = "permit the user"
841
+ Pundit::RSpec::Matchers.description = ->(user, record) do
842
+ "permit user with role #{user.role} to access record with ID #{record.id}"
843
+ end
844
+ ```
845
+
846
+ Which would make for a less chatty output:
847
+
848
+ ```
849
+ PostPolicy
850
+ update? and show?
851
+ is expected to permit user with role admin to access record with ID 130
852
+ ```
853
+
854
+ ### Focus Support
855
+
856
+ If your RSpec config has `filter_run_when_matching :focus`, you may tag the `permissions` helper like so:
857
+
858
+ ```
859
+ permissions :show?, :focus do
860
+ ```
788
861
 
789
862
  ### Scope Specs
790
863
 
@@ -803,15 +876,15 @@ inherit_gem:
803
876
  # External Resources
804
877
 
805
878
  - [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
806
- - [Migrating to Pundit from CanCan](http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
807
- - [Testing Pundit Policies with RSpec](http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
879
+ - [Migrating to Pundit from CanCan](https://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
880
+ - [Testing Pundit Policies with RSpec](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
808
881
  - [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450)
809
882
  - [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
810
- - [Straightforward Rails Authorization with Pundit](http://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
883
+ - [Straightforward Rails Authorization with Pundit](https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
811
884
 
812
885
  ## Other implementations
813
886
 
814
- - [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](http://flask.pocoo.org/) extension "heavily inspired by" Pundit
887
+ - [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](https://flask.pocoo.org/) extension "heavily inspired by" Pundit
815
888
 
816
889
  # License
817
890
 
data/Rakefile CHANGED
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  YARD::Rake::YardocTask.new do |t|
17
17
  t.files = ["lib/**/*.rb"]
18
+ t.stats_options = ["--list-undoc"]
18
19
  end
19
20
 
20
21
  task default: :spec
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
+ # @private
4
5
  module Generators
6
+ # @private
5
7
  class InstallGenerator < ::Rails::Generators::Base
6
8
  source_root File.expand_path("templates", __dir__)
7
9
 
8
10
  def copy_application_policy
9
- template "application_policy.rb", "app/policies/application_policy.rb"
11
+ template "application_policy.rb.tt", "app/policies/application_policy.rb"
10
12
  end
11
13
  end
12
14
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
+ # @private
4
5
  module Generators
6
+ # @private
5
7
  class PolicyGenerator < ::Rails::Generators::NamedBase
6
8
  source_root File.expand_path("templates", __dir__)
7
9
 
8
10
  def create_policy
9
- template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
11
+ template "policy.rb.tt", File.join("app/policies", class_path, "#{file_name}_policy.rb")
10
12
  end
11
13
 
12
14
  hook_for :test_framework
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @private
3
4
  module Rspec
5
+ # @private
4
6
  module Generators
7
+ # @private
5
8
  class PolicyGenerator < ::Rails::Generators::NamedBase
6
9
  source_root File.expand_path("templates", __dir__)
7
10
 
8
11
  def create_policy_spec
9
- template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
12
+ template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
10
13
  end
11
14
  end
12
15
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @private
3
4
  module TestUnit
5
+ # @private
4
6
  module Generators
7
+ # @private
5
8
  class PolicyGenerator < ::Rails::Generators::NamedBase
6
9
  source_root File.expand_path("templates", __dir__)
7
10
 
8
11
  def create_policy_test
9
- template "policy_test.rb", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
12
+ template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
10
13
  end
11
14
  end
12
15
  end
@@ -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