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
@@ -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
data/lib/pundit/rspec.rb CHANGED
@@ -1,10 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Array#to_sentence
4
+ require "active_support/core_ext/array/conversions"
5
+
3
6
  module Pundit
7
+ # Namespace for Pundit's RSpec integration.
4
8
  module RSpec
9
+ # Namespace for Pundit's RSpec matchers.
5
10
  module Matchers
6
11
  extend ::RSpec::Matchers::DSL
7
12
 
13
+ # @!method description=(description)
14
+ class << self
15
+ # Used to build a suitable description for the Pundit `permit` matcher.
16
+ # @api public
17
+ # @param value [String, Proc]
18
+ # @example
19
+ # Pundit::RSpec::Matchers.description = ->(user, record) do
20
+ # "permit user with role #{user.role} to access record with ID #{record.id}"
21
+ # end
22
+ attr_writer :description
23
+
24
+ # Used to retrieve a suitable description for the Pundit `permit` matcher.
25
+ # @api private
26
+ # @private
27
+ def description(user, record)
28
+ return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
29
+
30
+ @description
31
+ end
32
+ end
33
+
8
34
  # rubocop:disable Metrics/BlockLength
9
35
  matcher :permit do |user, record|
10
36
  match_proc = lambda do |policy|
@@ -22,15 +48,25 @@ module Pundit
22
48
  end
23
49
 
24
50
  failure_message_proc = lambda do |policy|
25
- was_were = @violating_permissions.count > 1 ? "were" : "was"
26
51
  "Expected #{policy} to grant #{permissions.to_sentence} on " \
27
- "#{record} but #{@violating_permissions.to_sentence} #{was_were} not granted"
52
+ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted"
28
53
  end
29
54
 
30
55
  failure_message_when_negated_proc = lambda do |policy|
31
- was_were = @violating_permissions.count > 1 ? "were" : "was"
32
56
  "Expected #{policy} not to grant #{permissions.to_sentence} on " \
33
- "#{record} but #{@violating_permissions.to_sentence} #{was_were} granted"
57
+ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted"
58
+ end
59
+
60
+ def was_or_were
61
+ if @violating_permissions.count > 1
62
+ "were"
63
+ else
64
+ "was"
65
+ end
66
+ end
67
+
68
+ description do
69
+ Pundit::RSpec::Matchers.description(user, record) || super()
34
70
  end
35
71
 
36
72
  if respond_to?(:match_when_negated)
@@ -39,26 +75,73 @@ module Pundit
39
75
  failure_message(&failure_message_proc)
40
76
  failure_message_when_negated(&failure_message_when_negated_proc)
41
77
  else
78
+ # :nocov:
79
+ # Compatibility with RSpec < 3.0, released 2014-06-01.
42
80
  match_for_should(&match_proc)
43
81
  match_for_should_not(&match_when_negated_proc)
44
82
  failure_message_for_should(&failure_message_proc)
45
83
  failure_message_for_should_not(&failure_message_when_negated_proc)
84
+ # :nocov:
85
+ end
86
+
87
+ if ::RSpec.respond_to?(:current_example)
88
+ def current_example
89
+ ::RSpec.current_example
90
+ end
91
+ else
92
+ # :nocov:
93
+ # Compatibility with RSpec < 3.0, released 2014-06-01.
94
+ def current_example
95
+ example
96
+ end
97
+ # :nocov:
46
98
  end
47
99
 
48
100
  def permissions
49
- current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example
50
- current_example.metadata[:permissions]
101
+ current_example.metadata.fetch(:permissions) do
102
+ raise KeyError, <<~ERROR.strip
103
+ No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
104
+ ERROR
105
+ end
51
106
  end
52
107
  end
53
108
  # rubocop:enable Metrics/BlockLength
54
109
  end
55
110
 
111
+ # Mixed in to all policy example groups to provide a DSL.
56
112
  module DSL
113
+ # @example
114
+ # describe PostPolicy do
115
+ # permissions :show?, :update? do
116
+ # it { is_expected.to permit(user, own_post) }
117
+ # end
118
+ # end
119
+ #
120
+ # @example focused example group
121
+ # describe PostPolicy do
122
+ # permissions :show?, :update?, :focus do
123
+ # it { is_expected.to permit(user, own_post) }
124
+ # end
125
+ # end
126
+ #
127
+ # @param list [Symbol, Array<Symbol>] a permission to describe
128
+ # @return [void]
57
129
  def permissions(*list, &block)
58
- describe(list.to_sentence, permissions: list, caller: caller) { instance_eval(&block) }
130
+ metadata = { permissions: list, caller: caller }
131
+
132
+ if list.last == :focus
133
+ list.pop
134
+ metadata[:focus] = true
135
+ end
136
+
137
+ description = list.to_sentence
138
+ describe(description, metadata) { instance_eval(&block) }
59
139
  end
60
140
  end
61
141
 
142
+ # Mixed in to all policy example groups.
143
+ #
144
+ # @private not useful
62
145
  module PolicyExampleGroup
63
146
  include Pundit::RSpec::Matchers
64
147
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
- VERSION = "2.3.2"
4
+ # The current version of Pundit.
5
+ VERSION = "2.5.0"
5
6
  end
data/lib/pundit.rb CHANGED
@@ -1,33 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+
3
5
  require "pundit/version"
4
6
  require "pundit/policy_finder"
5
- require "active_support/concern"
6
- require "active_support/core_ext/string/inflections"
7
- require "active_support/core_ext/object/blank"
8
- require "active_support/core_ext/module/introspection"
9
- require "active_support/dependencies/autoload"
10
7
  require "pundit/authorization"
11
8
  require "pundit/context"
9
+ require "pundit/cache_store"
12
10
  require "pundit/cache_store/null_store"
13
11
  require "pundit/cache_store/legacy_store"
12
+ require "pundit/railtie" if defined?(Rails)
14
13
 
15
14
  # @api private
16
15
  # To avoid name clashes with common Error naming when mixing in Pundit,
17
16
  # keep it here with compact class style definition.
18
17
  class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren
19
18
 
19
+ # Hello? Yes, this is Pundit.
20
+ #
20
21
  # @api public
21
22
  module Pundit
22
- SUFFIX = "Policy"
23
+ # @api private
24
+ # @deprecated See {Pundit::PolicyFinder}
25
+ SUFFIX = Pundit::PolicyFinder::SUFFIX
23
26
 
24
27
  # @api private
28
+ # @private
25
29
  module Generators; end
26
30
 
27
31
  # Error that will be raised when authorization has failed
28
32
  class NotAuthorizedError < Error
29
- attr_reader :query, :record, :policy
30
-
33
+ # @see #initialize
34
+ attr_reader :query
35
+ # @see #initialize
36
+ attr_reader :record
37
+ # @see #initialize
38
+ attr_reader :policy
39
+
40
+ # @overload initialize(message)
41
+ # Create an error with a simple error message.
42
+ # @param [String] message A simple error message string.
43
+ #
44
+ # @overload initialize(options)
45
+ # Create an error with the specified attributes.
46
+ # @param [Hash] options The error options.
47
+ # @option options [String] :message Optional custom error message. Will default to a generalized message.
48
+ # @option options [Symbol] :query The name of the policy method that was checked.
49
+ # @option options [Object] :record The object that was being checked with the policy.
50
+ # @option options [Class] :policy The class of policy that was used for the check.
31
51
  def initialize(options = {})
32
52
  if options.is_a? String
33
53
  message = options
@@ -36,7 +56,10 @@ module Pundit
36
56
  @record = options[:record]
37
57
  @policy = options[:policy]
38
58
 
39
- message = options.fetch(:message) { "not allowed to #{query} this #{record.class}" }
59
+ message = options.fetch(:message) do
60
+ record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}"
61
+ "not allowed to #{policy.class}##{query} #{record_name}"
62
+ end
40
63
  end
41
64
 
42
65
  super(message)
@@ -67,10 +90,11 @@ module Pundit
67
90
  end
68
91
 
69
92
  class << self
70
- # @see [Pundit::Context#authorize]
93
+ # @see Pundit::Context#authorize
71
94
  def authorize(user, record, query, policy_class: nil, cache: nil)
72
95
  context = if cache
73
- Context.new(user: user, policy_cache: cache)
96
+ policy_cache = CacheStore::LegacyStore.new(cache)
97
+ Context.new(user: user, policy_cache: policy_cache)
74
98
  else
75
99
  Context.new(user: user)
76
100
  end
@@ -78,29 +102,33 @@ module Pundit
78
102
  context.authorize(record, query: query, policy_class: policy_class)
79
103
  end
80
104
 
81
- # @see [Pundit::Context#policy_scope]
105
+ # @see Pundit::Context#policy_scope
82
106
  def policy_scope(user, *args, **kwargs, &block)
83
107
  Context.new(user: user).policy_scope(*args, **kwargs, &block)
84
108
  end
85
109
 
86
- # @see [Pundit::Context#policy_scope!]
110
+ # @see Pundit::Context#policy_scope!
87
111
  def policy_scope!(user, *args, **kwargs, &block)
88
112
  Context.new(user: user).policy_scope!(*args, **kwargs, &block)
89
113
  end
90
114
 
91
- # @see [Pundit::Context#policy]
115
+ # @see Pundit::Context#policy
92
116
  def policy(user, *args, **kwargs, &block)
93
117
  Context.new(user: user).policy(*args, **kwargs, &block)
94
118
  end
95
119
 
96
- # @see [Pundit::Context#policy!]
120
+ # @see Pundit::Context#policy!
97
121
  def policy!(user, *args, **kwargs, &block)
98
122
  Context.new(user: user).policy!(*args, **kwargs, &block)
99
123
  end
100
124
  end
101
125
 
126
+ # Rails view helpers, to allow a slightly different view-specific
127
+ # implementation of the methods in {Pundit::Authorization}.
128
+ #
102
129
  # @api private
103
130
  module Helper
131
+ # @see Pundit::Authorization#pundit_policy_scope
104
132
  def policy_scope(scope)
105
133
  pundit_policy_scope(scope)
106
134
  end
data/pundit.gemspec CHANGED
@@ -16,20 +16,16 @@ Gem::Specification.new do |gem|
16
16
 
17
17
  gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
18
18
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
19
  gem.require_paths = ["lib"]
21
20
 
22
- gem.metadata = { "rubygems_mfa_required" => "true" }
21
+ gem.metadata = {
22
+ "rubygems_mfa_required" => "true",
23
+ "bug_tracker_uri" => "https://github.com/varvet/pundit/issues",
24
+ "changelog_uri" => "https://github.com/varvet/pundit/blob/main/CHANGELOG.md",
25
+ "documentation_uri" => "https://github.com/varvet/pundit/blob/main/README.md",
26
+ "homepage_uri" => "https://github.com/varvet/pundit",
27
+ "source_code_uri" => "https://github.com/varvet/pundit"
28
+ }
23
29
 
24
30
  gem.add_dependency "activesupport", ">= 3.0.0"
25
- gem.add_development_dependency "actionpack", ">= 3.0.0"
26
- gem.add_development_dependency "activemodel", ">= 3.0.0"
27
- gem.add_development_dependency "bundler"
28
- gem.add_development_dependency "pry"
29
- gem.add_development_dependency "railties", ">= 3.0.0"
30
- gem.add_development_dependency "rake"
31
- gem.add_development_dependency "rspec", ">= 3.0.0"
32
- gem.add_development_dependency "rubocop"
33
- gem.add_development_dependency "simplecov", ">= 0.17.0"
34
- gem.add_development_dependency "yard"
35
31
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require "action_controller/metal/strong_parameters"
4
5
 
5
6
  describe Pundit::Authorization do
6
7
  def to_params(*args, **kwargs, &block)
@@ -8,7 +9,7 @@ describe Pundit::Authorization do
8
9
  end
9
10
 
10
11
  let(:controller) { Controller.new(user, "update", to_params({})) }
11
- let(:user) { double }
12
+ let(:user) { double("user") }
12
13
  let(:post) { Post.new(user) }
13
14
  let(:comment) { Comment.new }
14
15
  let(:article) { Article.new }
@@ -157,7 +158,7 @@ describe Pundit::Authorization do
157
158
  end
158
159
 
159
160
  it "allows policy to be injected" do
160
- new_policy = OpenStruct.new
161
+ new_policy = double
161
162
  controller.policies[post] = new_policy
162
163
 
163
164
  expect(controller.policy(post)).to eq new_policy
@@ -169,7 +170,7 @@ describe Pundit::Authorization do
169
170
  expect(controller.policy_scope(Post)).to eq :published
170
171
  end
171
172
 
172
- it "allows policy scope class to be overriden" do
173
+ it "allows policy scope class to be overridden" do
173
174
  expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
174
175
  end
175
176
 
@@ -182,7 +183,7 @@ describe Pundit::Authorization do
182
183
  end
183
184
 
184
185
  it "allows policy_scope to be injected" do
185
- new_scope = OpenStruct.new
186
+ new_scope = double
186
187
  controller.policy_scopes[Post] = new_scope
187
188
 
188
189
  expect(controller.policy_scope(Post)).to eq new_scope
@@ -271,4 +272,60 @@ describe Pundit::Authorization do
271
272
  expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
272
273
  end
273
274
  end
275
+
276
+ describe "#pundit_reset!" do
277
+ it "allows authorize to react to a user change" do
278
+ expect(controller.authorize(post)).to be_truthy
279
+
280
+ controller.current_user = double
281
+ controller.pundit_reset!
282
+ expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError)
283
+ end
284
+
285
+ it "allows policy to react to a user change" do
286
+ expect(controller.policy(DummyCurrentUser).user).to be user
287
+
288
+ new_user = double("new user")
289
+ controller.current_user = new_user
290
+ controller.pundit_reset!
291
+ expect(controller.policy(DummyCurrentUser).user).to be new_user
292
+ end
293
+
294
+ it "allows policy scope to react to a user change" do
295
+ expect(controller.policy_scope(DummyCurrentUser)).to be user
296
+
297
+ new_user = double("new user")
298
+ controller.current_user = new_user
299
+ controller.pundit_reset!
300
+ expect(controller.policy_scope(DummyCurrentUser)).to be new_user
301
+ end
302
+
303
+ it "resets the pundit context" do
304
+ expect(controller.pundit.user).to be(user)
305
+
306
+ new_user = double
307
+ controller.current_user = new_user
308
+ expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user)
309
+ end
310
+
311
+ it "clears pundit_policy_authorized? flag" do
312
+ expect(controller.pundit_policy_authorized?).to be false
313
+
314
+ controller.skip_authorization
315
+ expect(controller.pundit_policy_authorized?).to be true
316
+
317
+ controller.pundit_reset!
318
+ expect(controller.pundit_policy_authorized?).to be false
319
+ end
320
+
321
+ it "clears pundit_policy_scoped? flag" do
322
+ expect(controller.pundit_policy_scoped?).to be false
323
+
324
+ controller.skip_policy_scope
325
+ expect(controller.pundit_policy_scoped?).to be true
326
+
327
+ controller.pundit_reset!
328
+ expect(controller.pundit_policy_scoped?).to be false
329
+ end
330
+ end
274
331
  end
@@ -18,5 +18,32 @@ RSpec.describe PostPolicy do
18
18
  should permit(user, other_post)
19
19
  end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
20
20
  end
21
+
22
+ it "uses the default description if not overridden" do
23
+ expect(permit(user, own_post).description).to eq("permit #{user.inspect} and #{own_post.inspect}")
24
+ end
25
+
26
+ context "when the matcher description is overridden" do
27
+ after do
28
+ Pundit::RSpec::Matchers.description = nil
29
+ end
30
+
31
+ it "sets a custom matcher description with a Proc" do
32
+ allow(user).to receive(:role).and_return("default_role")
33
+ allow(own_post).to receive(:id).and_return(1)
34
+
35
+ Pundit::RSpec::Matchers.description = lambda { |user, record|
36
+ "permit user with role #{user.role} to access record with ID #{record.id}"
37
+ }
38
+
39
+ description = permit(user, own_post).description
40
+ expect(description).to eq("permit user with role default_role to access record with ID 1")
41
+ end
42
+
43
+ it "sets a custom matcher description with a string" do
44
+ Pundit::RSpec::Matchers.description = "permit user"
45
+ expect(permit(user, own_post).description).to eq("permit user")
46
+ end
47
+ end
21
48
  end
22
49
  end