pundit 2.3.0 → 2.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE/gem_release_template.md +8 -0
- data/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +9 -0
- data/.github/workflows/main.yml +107 -0
- data/.github/workflows/push_gem.yml +33 -0
- data/.rubocop.yml +7 -16
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +2 -5
- data/Gemfile +3 -2
- data/README.md +26 -38
- data/SECURITY.md +19 -0
- data/lib/generators/pundit/install/templates/application_policy.rb +1 -1
- data/lib/generators/pundit/policy/templates/policy.rb +7 -1
- data/lib/generators/rspec/templates/policy_spec.rb +1 -1
- data/lib/pundit/authorization.rb +12 -4
- data/lib/pundit/cache_store/legacy_store.rb +17 -0
- data/lib/pundit/cache_store/null_store.rb +18 -0
- data/lib/pundit/context.rb +127 -0
- data/lib/pundit/policy_finder.rb +1 -1
- data/lib/pundit/version.rb +1 -1
- data/lib/pundit.rb +24 -88
- data/pundit.gemspec +4 -2
- data/spec/authorization_spec.rb +22 -6
- data/spec/generators_spec.rb +1 -1
- data/spec/pundit_spec.rb +14 -10
- data/spec/spec_helper.rb +112 -35
- metadata +21 -13
- data/.travis.yml +0 -26
@@ -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
|
data/lib/pundit/policy_finder.rb
CHANGED
@@ -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)
|
data/lib/pundit/version.rb
CHANGED
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
|
-
|
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
|
-
#
|
66
|
-
|
67
|
-
|
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
|
-
|
75
|
+
Context.new(user: user)
|
82
76
|
end
|
83
77
|
|
84
|
-
|
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
|
-
#
|
110
|
-
|
111
|
-
|
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
|
-
#
|
131
|
-
|
132
|
-
|
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
|
-
#
|
145
|
-
|
146
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
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", "
|
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"
|
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
|
data/spec/authorization_spec.rb
CHANGED
@@ -3,10 +3,13 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
5
|
describe Pundit::Authorization do
|
6
|
-
|
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 =
|
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
|
-
|
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 =
|
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 =
|
261
|
+
params = to_params(
|
246
262
|
post: {
|
247
263
|
title: "Hello",
|
248
264
|
body: "blah",
|
data/spec/generators_spec.rb
CHANGED
@@ -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(
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
|