pundit 2.1.1 → 2.4.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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +26 -0
- data/.github/PULL_REQUEST_TEMPLATE/gem_release_template.md +8 -0
- data/.github/pull_request_template.md +9 -0
- data/.github/workflows/main.yml +112 -0
- data/.github/workflows/push_gem.yml +33 -0
- data/.rubocop.yml +14 -18
- data/CHANGELOG.md +52 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +3 -5
- data/Gemfile +3 -2
- data/README.md +117 -62
- data/SECURITY.md +19 -0
- data/config/rubocop-rspec.yml +5 -0
- data/lib/generators/pundit/install/templates/application_policy.rb +1 -1
- data/lib/generators/pundit/policy/templates/policy.rb +11 -4
- data/lib/generators/rspec/templates/policy_spec.rb +1 -1
- data/lib/pundit/authorization.rb +176 -0
- 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/rspec.rb +23 -1
- data/lib/pundit/version.rb +1 -1
- data/lib/pundit.rb +35 -249
- data/pundit.gemspec +5 -2
- data/spec/authorization_spec.rb +274 -0
- data/spec/dsl_spec.rb +30 -0
- data/spec/generators_spec.rb +43 -0
- data/spec/policies/post_policy_spec.rb +27 -0
- data/spec/policy_finder_spec.rb +1 -1
- data/spec/pundit_spec.rb +59 -244
- data/spec/spec_helper.rb +123 -39
- metadata +42 -10
- data/.travis.yml +0 -25
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Pundit::Authorization do
|
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({})) }
|
11
|
+
let(:user) { double }
|
12
|
+
let(:post) { Post.new(user) }
|
13
|
+
let(:comment) { Comment.new }
|
14
|
+
let(:article) { Article.new }
|
15
|
+
let(:article_tag) { ArticleTag.new }
|
16
|
+
let(:wiki) { Wiki.new }
|
17
|
+
|
18
|
+
describe "#verify_authorized" do
|
19
|
+
it "does nothing when authorized" do
|
20
|
+
controller.authorize(post)
|
21
|
+
controller.verify_authorized
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises an exception when not authorized" do
|
25
|
+
expect { controller.verify_authorized }.to raise_error(Pundit::AuthorizationNotPerformedError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#verify_policy_scoped" do
|
30
|
+
it "does nothing when policy_scope is used" do
|
31
|
+
controller.policy_scope(Post)
|
32
|
+
controller.verify_policy_scoped
|
33
|
+
end
|
34
|
+
|
35
|
+
it "raises an exception when policy_scope is not used" do
|
36
|
+
expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#pundit_policy_authorized?" do
|
41
|
+
it "is true when authorized" do
|
42
|
+
controller.authorize(post)
|
43
|
+
expect(controller.pundit_policy_authorized?).to be true
|
44
|
+
end
|
45
|
+
|
46
|
+
it "is false when not authorized" do
|
47
|
+
expect(controller.pundit_policy_authorized?).to be false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#pundit_policy_scoped?" do
|
52
|
+
it "is true when policy_scope is used" do
|
53
|
+
controller.policy_scope(Post)
|
54
|
+
expect(controller.pundit_policy_scoped?).to be true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "is false when policy scope is not used" do
|
58
|
+
expect(controller.pundit_policy_scoped?).to be false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#authorize" do
|
63
|
+
it "infers the policy name and authorizes based on it" do
|
64
|
+
expect(controller.authorize(post)).to be_truthy
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns the record on successful authorization" do
|
68
|
+
expect(controller.authorize(post)).to eq(post)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "returns the record when passed record with namespace " do
|
72
|
+
expect(controller.authorize([:project, comment], :update?)).to eq(comment)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns the record when passed record with nested namespace " do
|
76
|
+
expect(controller.authorize([:project, :admin, comment], :update?)).to eq(comment)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns the policy name symbol when passed record with headless policy" do
|
80
|
+
expect(controller.authorize(:publication, :create?)).to eq(:publication)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns the class when passed record not a particular instance" do
|
84
|
+
expect(controller.authorize(Post, :show?)).to eq(Post)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "can be given a different permission to check" do
|
88
|
+
expect(controller.authorize(post, :show?)).to be_truthy
|
89
|
+
expect { controller.authorize(post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "can be given a different policy class" do
|
93
|
+
expect(controller.authorize(post, :create?, policy_class: PublicationPolicy)).to be_truthy
|
94
|
+
end
|
95
|
+
|
96
|
+
it "works with anonymous class policies" do
|
97
|
+
expect(controller.authorize(article_tag, :show?)).to be_truthy
|
98
|
+
expect { controller.authorize(article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "throws an exception when the permission check fails" do
|
102
|
+
expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "throws an exception when a policy cannot be found" do
|
106
|
+
expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "caches the policy" do
|
110
|
+
expect(controller.policies[post]).to be_nil
|
111
|
+
controller.authorize(post)
|
112
|
+
expect(controller.policies[post]).not_to be_nil
|
113
|
+
end
|
114
|
+
|
115
|
+
it "raises an error when the given record is nil" do
|
116
|
+
expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "raises an error with a invalid policy constructor" do
|
120
|
+
expect { controller.authorize(wiki, :destroy?) }.to raise_error(Pundit::InvalidConstructorError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#skip_authorization" do
|
125
|
+
it "disables authorization verification" do
|
126
|
+
controller.skip_authorization
|
127
|
+
expect { controller.verify_authorized }.not_to raise_error
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "#skip_policy_scope" do
|
132
|
+
it "disables policy scope verification" do
|
133
|
+
controller.skip_policy_scope
|
134
|
+
expect { controller.verify_policy_scoped }.not_to raise_error
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#pundit_user" do
|
139
|
+
it "returns the same thing as current_user" do
|
140
|
+
expect(controller.pundit_user).to eq controller.current_user
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "#policy" do
|
145
|
+
it "returns an instantiated policy" do
|
146
|
+
policy = controller.policy(post)
|
147
|
+
expect(policy.user).to eq user
|
148
|
+
expect(policy.post).to eq post
|
149
|
+
end
|
150
|
+
|
151
|
+
it "throws an exception if the given policy can't be found" do
|
152
|
+
expect { controller.policy(article) }.to raise_error(Pundit::NotDefinedError)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "raises an error with a invalid policy constructor" do
|
156
|
+
expect { controller.policy(wiki) }.to raise_error(Pundit::InvalidConstructorError)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "allows policy to be injected" do
|
160
|
+
new_policy = OpenStruct.new
|
161
|
+
controller.policies[post] = new_policy
|
162
|
+
|
163
|
+
expect(controller.policy(post)).to eq new_policy
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "#policy_scope" do
|
168
|
+
it "returns an instantiated policy scope" do
|
169
|
+
expect(controller.policy_scope(Post)).to eq :published
|
170
|
+
end
|
171
|
+
|
172
|
+
it "allows policy scope class to be overridden" do
|
173
|
+
expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
|
174
|
+
end
|
175
|
+
|
176
|
+
it "throws an exception if the given policy can't be found" do
|
177
|
+
expect { controller.policy_scope(Article) }.to raise_error(Pundit::NotDefinedError)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "raises an error with a invalid policy scope constructor" do
|
181
|
+
expect { controller.policy_scope(Wiki) }.to raise_error(Pundit::InvalidConstructorError)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "allows policy_scope to be injected" do
|
185
|
+
new_scope = OpenStruct.new
|
186
|
+
controller.policy_scopes[Post] = new_scope
|
187
|
+
|
188
|
+
expect(controller.policy_scope(Post)).to eq new_scope
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "#permitted_attributes" do
|
193
|
+
it "checks policy for permitted attributes" do
|
194
|
+
params = to_params(
|
195
|
+
post: {
|
196
|
+
title: "Hello",
|
197
|
+
votes: 5,
|
198
|
+
admin: true
|
199
|
+
}
|
200
|
+
)
|
201
|
+
|
202
|
+
action = "update"
|
203
|
+
|
204
|
+
expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq(
|
205
|
+
"title" => "Hello",
|
206
|
+
"votes" => 5
|
207
|
+
)
|
208
|
+
expect(Controller.new(double, action, params).permitted_attributes(post).to_h).to eq("votes" => 5)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "checks policy for permitted attributes for record of a ActiveModel type" do
|
212
|
+
customer_post = Customer::Post.new(user)
|
213
|
+
params = to_params(
|
214
|
+
customer_post: {
|
215
|
+
title: "Hello",
|
216
|
+
votes: 5,
|
217
|
+
admin: true
|
218
|
+
}
|
219
|
+
)
|
220
|
+
|
221
|
+
action = "update"
|
222
|
+
|
223
|
+
expect(Controller.new(user, action, params).permitted_attributes(customer_post).to_h).to eq(
|
224
|
+
"title" => "Hello",
|
225
|
+
"votes" => 5
|
226
|
+
)
|
227
|
+
expect(Controller.new(double, action, params).permitted_attributes(customer_post).to_h).to eq(
|
228
|
+
"votes" => 5
|
229
|
+
)
|
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
|
243
|
+
end
|
244
|
+
|
245
|
+
describe "#permitted_attributes_for_action" do
|
246
|
+
it "is checked if it is defined in the policy" do
|
247
|
+
params = to_params(
|
248
|
+
post: {
|
249
|
+
title: "Hello",
|
250
|
+
body: "blah",
|
251
|
+
votes: 5,
|
252
|
+
admin: true
|
253
|
+
}
|
254
|
+
)
|
255
|
+
|
256
|
+
action = "revise"
|
257
|
+
expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah")
|
258
|
+
end
|
259
|
+
|
260
|
+
it "can be explicitly set" do
|
261
|
+
params = to_params(
|
262
|
+
post: {
|
263
|
+
title: "Hello",
|
264
|
+
body: "blah",
|
265
|
+
votes: 5,
|
266
|
+
admin: true
|
267
|
+
}
|
268
|
+
)
|
269
|
+
|
270
|
+
action = "update"
|
271
|
+
expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Pundit RSpec DSL" do
|
6
|
+
let(:fake_rspec) do
|
7
|
+
double = class_double(RSpec::ExampleGroups)
|
8
|
+
double.extend(::Pundit::RSpec::DSL)
|
9
|
+
double
|
10
|
+
end
|
11
|
+
let(:block) { proc { "block content" } }
|
12
|
+
|
13
|
+
it "calls describe with the correct metadata and without :focus" do
|
14
|
+
expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) }
|
15
|
+
expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
|
16
|
+
expect(block.call).to eq("block content")
|
17
|
+
end
|
18
|
+
|
19
|
+
fake_rspec.permissions(:item1, :item2, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "calls describe with the correct metadata and with :focus" do
|
23
|
+
expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true }
|
24
|
+
expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
|
25
|
+
expect(block.call).to eq("block content")
|
26
|
+
end
|
27
|
+
|
28
|
+
fake_rspec.permissions(:item1, :item2, :focus, &block)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tmpdir"
|
5
|
+
|
6
|
+
require "rails/generators"
|
7
|
+
require "generators/pundit/install/install_generator"
|
8
|
+
require "generators/pundit/policy/policy_generator"
|
9
|
+
|
10
|
+
RSpec.describe "generators" do
|
11
|
+
before(:all) do
|
12
|
+
@tmpdir = Dir.mktmpdir
|
13
|
+
|
14
|
+
Dir.chdir(@tmpdir) do
|
15
|
+
Pundit::Generators::InstallGenerator.new([], { quiet: true }).invoke_all
|
16
|
+
Pundit::Generators::PolicyGenerator.new(%w[Widget], { quiet: true }).invoke_all
|
17
|
+
|
18
|
+
require "./app/policies/application_policy"
|
19
|
+
require "./app/policies/widget_policy"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
after(:all) do
|
24
|
+
FileUtils.remove_entry(@tmpdir)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "WidgetPolicy", type: :policy do
|
28
|
+
permissions :index?, :show?, :create?, :new?, :update?, :edit?, :destroy? do
|
29
|
+
it "has safe defaults" do
|
30
|
+
expect(WidgetPolicy).not_to permit(double("User"), double("Widget"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "WidgetPolicy::Scope" do
|
35
|
+
describe "#resolve" do
|
36
|
+
it "raises a descriptive error" do
|
37
|
+
scope = WidgetPolicy::Scope.new(double("User"), double("User.all"))
|
38
|
+
expect { scope.resolve }.to raise_error(NoMethodError, /WidgetPolicy::Scope/)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
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
|
data/spec/policy_finder_spec.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
|
+
class Foo; end
|
5
6
|
RSpec.describe Pundit::PolicyFinder do
|
6
7
|
let(:user) { double }
|
7
8
|
let(:post) { Post.new(user) }
|
@@ -114,7 +115,6 @@ RSpec.describe Pundit::PolicyFinder do
|
|
114
115
|
|
115
116
|
context "with a class that doesn't have an associated policy" do
|
116
117
|
it "returns nil" do
|
117
|
-
class Foo; end
|
118
118
|
object = described_class.new(Foo)
|
119
119
|
|
120
120
|
expect(object.policy).to eq nil
|