pundit 2.3.0 → 2.3.2

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.
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ class Context
5
+ def initialize(user:, policy_cache: CacheStore::NullStore.instance)
6
+ @user = user
7
+ @policy_cache = policy_cache
8
+ end
9
+
10
+ attr_reader :user
11
+
12
+ # @api private
13
+ attr_reader :policy_cache
14
+
15
+ # Retrieves the policy for the given record, initializing it with the
16
+ # record and user and finally throwing an error if the user is not
17
+ # authorized to perform the given action.
18
+ #
19
+ # @param user [Object] the user that initiated the action
20
+ # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
21
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
22
+ # @param policy_class [Class] the policy class we want to force use of
23
+ # @raise [NotAuthorizedError] if the given query method returned false
24
+ # @return [Object] Always returns the passed object record
25
+ def authorize(possibly_namespaced_record, query:, policy_class:)
26
+ record = pundit_model(possibly_namespaced_record)
27
+ policy = if policy_class
28
+ policy_class.new(user, record)
29
+ else
30
+ policy!(possibly_namespaced_record)
31
+ end
32
+
33
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
34
+
35
+ record
36
+ end
37
+
38
+ # Retrieves the policy scope for the given record.
39
+ #
40
+ # @see https://github.com/varvet/pundit#scopes
41
+ # @param user [Object] the user that initiated the action
42
+ # @param scope [Object] the object we're retrieving the policy scope for
43
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
44
+ # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
45
+ def policy_scope(scope)
46
+ policy_scope_class = policy_finder(scope).scope
47
+ return unless policy_scope_class
48
+
49
+ begin
50
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
51
+ rescue ArgumentError
52
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
53
+ end
54
+
55
+ policy_scope.resolve
56
+ end
57
+
58
+ # Retrieves the policy scope for the given record. Raises if not found.
59
+ #
60
+ # @see https://github.com/varvet/pundit#scopes
61
+ # @param user [Object] the user that initiated the action
62
+ # @param scope [Object] the object we're retrieving the policy scope for
63
+ # @raise [NotDefinedError] if the policy scope cannot be found
64
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
65
+ # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
66
+ def policy_scope!(scope)
67
+ policy_scope_class = policy_finder(scope).scope!
68
+ return unless policy_scope_class
69
+
70
+ begin
71
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
72
+ rescue ArgumentError
73
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
74
+ end
75
+
76
+ policy_scope.resolve
77
+ end
78
+
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
101
+
102
+ private
103
+
104
+ def cached_find(record)
105
+ policy_cache.fetch(user: user, record: record) do
106
+ klass = yield policy_finder(record)
107
+ next unless klass
108
+
109
+ model = pundit_model(record)
110
+
111
+ begin
112
+ klass.new(user, model)
113
+ rescue ArgumentError
114
+ raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
115
+ end
116
+ end
117
+ end
118
+
119
+ def policy_finder(record)
120
+ PolicyFinder.new(record)
121
+ end
122
+
123
+ def pundit_model(record)
124
+ record.is_a?(Array) ? record.last : record
125
+ end
126
+ end
127
+ end
@@ -56,7 +56,7 @@ module Pundit
56
56
 
57
57
  # @return [String] the name of the key this object would have in a params hash
58
58
  #
59
- def param_key
59
+ def param_key # rubocop:disable Metrics/AbcSize
60
60
  model = object.is_a?(Array) ? object.last : object
61
61
 
62
62
  if model.respond_to?(:model_name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
- VERSION = "2.3.0"
4
+ VERSION = "2.3.2"
5
5
  end
data/lib/pundit.rb CHANGED
@@ -8,6 +8,9 @@ require "active_support/core_ext/object/blank"
8
8
  require "active_support/core_ext/module/introspection"
9
9
  require "active_support/dependencies/autoload"
10
10
  require "pundit/authorization"
11
+ require "pundit/context"
12
+ require "pundit/cache_store/null_store"
13
+ require "pundit/cache_store/legacy_store"
11
14
 
12
15
  # @api private
13
16
  # To avoid name clashes with common Error naming when mixing in Pundit,
@@ -55,111 +58,44 @@ module Pundit
55
58
  class NotDefinedError < Error; end
56
59
 
57
60
  def self.included(base)
58
- ActiveSupport::Deprecation.warn <<~WARNING
61
+ location = caller_locations(1, 1).first
62
+ warn <<~WARNING
59
63
  'include Pundit' is deprecated. Please use 'include Pundit::Authorization' instead.
64
+ (called from #{location.label} at #{location.path}:#{location.lineno})
60
65
  WARNING
61
66
  base.include Authorization
62
67
  end
63
68
 
64
69
  class << self
65
- # Retrieves the policy for the given record, initializing it with the
66
- # record and user and finally throwing an error if the user is not
67
- # authorized to perform the given action.
68
- #
69
- # @param user [Object] the user that initiated the action
70
- # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
71
- # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
72
- # @param policy_class [Class] the policy class we want to force use of
73
- # @param cache [#[], #[]=] a Hash-like object to cache the found policy instance in
74
- # @raise [NotAuthorizedError] if the given query method returned false
75
- # @return [Object] Always returns the passed object record
76
- def authorize(user, possibly_namespaced_record, query, policy_class: nil, cache: {})
77
- record = pundit_model(possibly_namespaced_record)
78
- policy = if policy_class
79
- policy_class.new(user, record)
70
+ # @see [Pundit::Context#authorize]
71
+ def authorize(user, record, query, policy_class: nil, cache: nil)
72
+ context = if cache
73
+ Context.new(user: user, policy_cache: cache)
80
74
  else
81
- cache[possibly_namespaced_record] ||= policy!(user, possibly_namespaced_record)
75
+ Context.new(user: user)
82
76
  end
83
77
 
84
- raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
85
-
86
- record
87
- end
88
-
89
- # Retrieves the policy scope for the given record.
90
- #
91
- # @see https://github.com/varvet/pundit#scopes
92
- # @param user [Object] the user that initiated the action
93
- # @param scope [Object] the object we're retrieving the policy scope for
94
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
95
- # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
96
- def policy_scope(user, scope)
97
- policy_scope_class = PolicyFinder.new(scope).scope
98
- return unless policy_scope_class
99
-
100
- begin
101
- policy_scope = policy_scope_class.new(user, pundit_model(scope))
102
- rescue ArgumentError
103
- raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
104
- end
105
-
106
- policy_scope.resolve
78
+ context.authorize(record, query: query, policy_class: policy_class)
107
79
  end
108
80
 
109
- # Retrieves the policy scope for the given record.
110
- #
111
- # @see https://github.com/varvet/pundit#scopes
112
- # @param user [Object] the user that initiated the action
113
- # @param scope [Object] the object we're retrieving the policy scope for
114
- # @raise [NotDefinedError] if the policy scope cannot be found
115
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
116
- # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
117
- def policy_scope!(user, scope)
118
- policy_scope_class = PolicyFinder.new(scope).scope!
119
- return unless policy_scope_class
120
-
121
- begin
122
- policy_scope = policy_scope_class.new(user, pundit_model(scope))
123
- rescue ArgumentError
124
- raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
125
- end
126
-
127
- policy_scope.resolve
81
+ # @see [Pundit::Context#policy_scope]
82
+ def policy_scope(user, *args, **kwargs, &block)
83
+ Context.new(user: user).policy_scope(*args, **kwargs, &block)
128
84
  end
129
85
 
130
- # Retrieves the policy for the given record.
131
- #
132
- # @see https://github.com/varvet/pundit#policies
133
- # @param user [Object] the user that initiated the action
134
- # @param record [Object] the object we're retrieving the policy for
135
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
136
- # @return [Object, nil] instance of policy class with query methods
137
- def policy(user, record)
138
- policy = PolicyFinder.new(record).policy
139
- policy&.new(user, pundit_model(record))
140
- rescue ArgumentError
141
- raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
86
+ # @see [Pundit::Context#policy_scope!]
87
+ def policy_scope!(user, *args, **kwargs, &block)
88
+ Context.new(user: user).policy_scope!(*args, **kwargs, &block)
142
89
  end
143
90
 
144
- # Retrieves the policy for the given record.
145
- #
146
- # @see https://github.com/varvet/pundit#policies
147
- # @param user [Object] the user that initiated the action
148
- # @param record [Object] the object we're retrieving the policy for
149
- # @raise [NotDefinedError] if the policy cannot be found
150
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
151
- # @return [Object] instance of policy class with query methods
152
- def policy!(user, record)
153
- policy = PolicyFinder.new(record).policy!
154
- policy.new(user, pundit_model(record))
155
- rescue ArgumentError
156
- raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
91
+ # @see [Pundit::Context#policy]
92
+ def policy(user, *args, **kwargs, &block)
93
+ Context.new(user: user).policy(*args, **kwargs, &block)
157
94
  end
158
95
 
159
- private
160
-
161
- def pundit_model(record)
162
- record.is_a?(Array) ? record.last : record
96
+ # @see [Pundit::Context#policy!]
97
+ def policy!(user, *args, **kwargs, &block)
98
+ Context.new(user: user).policy!(*args, **kwargs, &block)
163
99
  end
164
100
  end
165
101
 
data/pundit.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
8
8
  gem.name = "pundit"
9
9
  gem.version = Pundit::VERSION
10
10
  gem.authors = ["Jonas Nicklas", "Varvet AB"]
11
- gem.email = ["jonas.nicklas@gmail.com", "dev@elabs.se"]
11
+ gem.email = ["jonas.nicklas@gmail.com", "info@varvet.com"]
12
12
  gem.description = "Object oriented authorization for Rails applications"
13
13
  gem.summary = "OO authorization for Rails"
14
14
  gem.homepage = "https://github.com/varvet/pundit"
@@ -19,6 +19,8 @@ Gem::Specification.new do |gem|
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
20
  gem.require_paths = ["lib"]
21
21
 
22
+ gem.metadata = { "rubygems_mfa_required" => "true" }
23
+
22
24
  gem.add_dependency "activesupport", ">= 3.0.0"
23
25
  gem.add_development_dependency "actionpack", ">= 3.0.0"
24
26
  gem.add_development_dependency "activemodel", ">= 3.0.0"
@@ -27,7 +29,7 @@ Gem::Specification.new do |gem|
27
29
  gem.add_development_dependency "railties", ">= 3.0.0"
28
30
  gem.add_development_dependency "rake"
29
31
  gem.add_development_dependency "rspec", ">= 3.0.0"
30
- gem.add_development_dependency "rubocop", "1.24.0"
32
+ gem.add_development_dependency "rubocop"
31
33
  gem.add_development_dependency "simplecov", ">= 0.17.0"
32
34
  gem.add_development_dependency "yard"
33
35
  end
@@ -3,10 +3,13 @@
3
3
  require "spec_helper"
4
4
 
5
5
  describe Pundit::Authorization do
6
- let(:controller) { Controller.new(user, "update", {}) }
6
+ def to_params(*args, **kwargs, &block)
7
+ ActionController::Parameters.new(*args, **kwargs, &block)
8
+ end
9
+
10
+ let(:controller) { Controller.new(user, "update", to_params({})) }
7
11
  let(:user) { double }
8
12
  let(:post) { Post.new(user) }
9
- let(:customer_post) { Customer::Post.new(user) }
10
13
  let(:comment) { Comment.new }
11
14
  let(:article) { Article.new }
12
15
  let(:article_tag) { ArticleTag.new }
@@ -188,7 +191,7 @@ describe Pundit::Authorization do
188
191
 
189
192
  describe "#permitted_attributes" do
190
193
  it "checks policy for permitted attributes" do
191
- params = ActionController::Parameters.new(
194
+ params = to_params(
192
195
  post: {
193
196
  title: "Hello",
194
197
  votes: 5,
@@ -206,7 +209,8 @@ describe Pundit::Authorization do
206
209
  end
207
210
 
208
211
  it "checks policy for permitted attributes for record of a ActiveModel type" do
209
- params = ActionController::Parameters.new(
212
+ customer_post = Customer::Post.new(user)
213
+ params = to_params(
210
214
  customer_post: {
211
215
  title: "Hello",
212
216
  votes: 5,
@@ -224,11 +228,23 @@ describe Pundit::Authorization do
224
228
  "votes" => 5
225
229
  )
226
230
  end
231
+
232
+ it "goes through the policy cache" do
233
+ params = to_params(post: { title: "Hello" })
234
+ user = double
235
+ post = Post.new(user)
236
+ controller = Controller.new(user, "update", params)
237
+
238
+ expect do
239
+ expect(controller.permitted_attributes(post)).to be_truthy
240
+ expect(controller.permitted_attributes(post)).to be_truthy
241
+ end.to change { PostPolicy.instances }.by(1)
242
+ end
227
243
  end
228
244
 
229
245
  describe "#permitted_attributes_for_action" do
230
246
  it "is checked if it is defined in the policy" do
231
- params = ActionController::Parameters.new(
247
+ params = to_params(
232
248
  post: {
233
249
  title: "Hello",
234
250
  body: "blah",
@@ -242,7 +258,7 @@ describe Pundit::Authorization do
242
258
  end
243
259
 
244
260
  it "can be explicitly set" do
245
- params = ActionController::Parameters.new(
261
+ params = to_params(
246
262
  post: {
247
263
  title: "Hello",
248
264
  body: "blah",
@@ -35,7 +35,7 @@ RSpec.describe "generators" do
35
35
  describe "#resolve" do
36
36
  it "raises a descriptive error" do
37
37
  scope = WidgetPolicy::Scope.new(double("User"), double("User.all"))
38
- expect { scope.resolve }.to raise_error(NotImplementedError, /WidgetPolicy::Scope/)
38
+ expect { scope.resolve }.to raise_error(NoMethodError, /WidgetPolicy::Scope/)
39
39
  end
40
40
  end
41
41
  end
data/spec/pundit_spec.rb CHANGED
@@ -64,7 +64,11 @@ RSpec.describe Pundit do
64
64
  end.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this Post") do |error|
65
65
  expect(error.query).to eq :destroy?
66
66
  expect(error.record).to eq post
67
- expect(error.policy).to eq Pundit.policy(user, post)
67
+ expect(error.policy).to have_attributes(
68
+ user: user,
69
+ record: post
70
+ )
71
+ expect(error.policy).to be_a(PostPolicy)
68
72
  end
69
73
  # rubocop:enable Style/MultilineBlockChain
70
74
  end
@@ -76,7 +80,11 @@ RSpec.describe Pundit do
76
80
  end.to raise_error(Pundit::NotAuthorizedError, "not allowed to destroy? this Comment") do |error|
77
81
  expect(error.query).to eq :destroy?
78
82
  expect(error.record).to eq comment
79
- expect(error.policy).to eq Pundit.policy(user, [:project, :admin, comment])
83
+ expect(error.policy).to have_attributes(
84
+ user: user,
85
+ record: comment
86
+ )
87
+ expect(error.policy).to be_a(Project::Admin::CommentPolicy)
80
88
  end
81
89
  # rubocop:enable Style/MultilineBlockChain
82
90
  end
@@ -399,22 +407,18 @@ RSpec.describe Pundit do
399
407
  it "includes Authorization module" do
400
408
  klass = Class.new
401
409
 
402
- ActiveSupport::Deprecation.silence do
410
+ expect do
403
411
  klass.include Pundit
404
- end
412
+ end.to output.to_stderr
405
413
 
406
414
  expect(klass).to include Pundit::Authorization
407
415
  end
408
416
 
409
417
  it "warns about deprecation" do
410
418
  klass = Class.new
411
- allow(ActiveSupport::Deprecation).to receive(:warn)
412
-
413
- ActiveSupport::Deprecation.silence do
419
+ expect do
414
420
  klass.include Pundit
415
- end
416
-
417
- expect(ActiveSupport::Deprecation).to have_received(:warn).with start_with("'include Pundit' is deprecated")
421
+ end.to output(a_string_starting_with("'include Pundit' is deprecated")).to_stderr
418
422
  end
419
423
  end
420
424