pundit 2.3.0 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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