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.
- 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
|
|