Skip to content

Commit 24bfed7

Browse files
authored
Authorize replacing of a polymorphic has-one relationship (#75)
* register replace_polymorphic_to_one_relationship callback * Authorize replacing of a polymorphic has-one relationship * Modify has-one relationship PATCH spec to use different original class * Fix polymorphic replace to work even when type changes
1 parent 87b8b12 commit 24bfed7

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed

lib/jsonapi/authorization/authorizing_processor.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class AuthorizingProcessor < JSONAPI::Processor
1616
set_callback :create_to_many_relationships, :before, :authorize_create_to_many_relationships
1717
set_callback :replace_to_many_relationships, :before, :authorize_replace_to_many_relationships
1818
set_callback :remove_to_many_relationships, :before, :authorize_remove_to_many_relationships
19+
set_callback(
20+
:replace_polymorphic_to_one_relationship,
21+
:before,
22+
:authorize_replace_polymorphic_to_one_relationship
23+
)
1924

2025
[
2126
:find,
@@ -216,6 +221,39 @@ def authorize_remove_to_one_relationship
216221
authorizer.remove_to_one_relationship(source_record, relationship_type)
217222
end
218223

224+
def authorize_replace_polymorphic_to_one_relationship
225+
return authorize_remove_to_one_relationship if params[:key_value].nil?
226+
227+
source_resource = @resource_klass.find_by_key(
228+
params[:resource_id],
229+
context: context
230+
)
231+
source_record = source_resource._model
232+
233+
# Fetch the name of the new class based on the incoming polymorphic
234+
# "type" value. This will fail if there is no associated resource for the
235+
# incoming "type" value so this shouldn't leak constants
236+
related_record_class_name = source_resource
237+
.send(:_model_class_name, params[:key_type])
238+
239+
# Fetch the underlying Resource class for the new record to-be-associated
240+
related_resource_klass = @resource_klass.resource_for(related_record_class_name)
241+
242+
new_related_resource = related_resource_klass
243+
.find_by_key(
244+
params[:key_value],
245+
context: context
246+
)
247+
new_related_record = new_related_resource._model unless new_related_resource.nil?
248+
249+
relationship_type = params[:relationship_type].to_sym
250+
authorizer.replace_to_one_relationship(
251+
source_record,
252+
new_related_record,
253+
relationship_type
254+
)
255+
end
256+
219257
private
220258

221259
def authorizer

spec/requests/relationship_operations_spec.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,86 @@
259259
end
260260
end
261261

262+
# Polymorphic has-one relationship replacing
263+
describe 'PATCH /tags/:id/relationships/taggable' do
264+
subject(:last_response) { patch("/tags/#{tag.id}/relationships/taggable", json) }
265+
266+
let!(:old_taggable) { Comment.create }
267+
let!(:tag) { Tag.create(taggable: old_taggable) }
268+
let(:policy_scope) { Article.all }
269+
let(:comment_policy_scope) { Article.all }
270+
let(:tag_policy_scope) { Tag.all }
271+
272+
before do
273+
allow_any_instance_of(TagPolicy::Scope).to receive(:resolve).and_return(tag_policy_scope)
274+
allow_any_instance_of(CommentPolicy::Scope).to receive(:resolve).and_return(comment_policy_scope)
275+
end
276+
277+
describe 'when replacing with a new taggable' do
278+
let!(:new_taggable) { Article.create(external_id: 'new-article-id') }
279+
let(:json) do
280+
<<-EOS.strip_heredoc
281+
{
282+
"data": {
283+
"type": "articles",
284+
"id": "#{new_taggable.external_id}"
285+
}
286+
}
287+
EOS
288+
end
289+
290+
context 'unauthorized for replace_to_one_relationship' do
291+
before { disallow_operation('replace_to_one_relationship', tag, new_taggable, :taggable) }
292+
it { is_expected.to be_forbidden }
293+
end
294+
295+
context 'authorized for replace_to_one_relationship' do
296+
before { allow_operation('replace_to_one_relationship', tag, new_taggable, :taggable) }
297+
it { is_expected.to be_successful }
298+
299+
context 'limited by policy scope on taggable', skip: 'DISCUSS' do
300+
let(:policy_scope) { Article.where.not(id: tag.taggable.id) }
301+
it { is_expected.to be_not_found }
302+
end
303+
304+
# If this happens in real life, it's mostly a bug. We want to document the
305+
# behaviour in that case anyway, as it might be surprising.
306+
context 'limited by policy scope on tag' do
307+
let(:tag_policy_scope) { Tag.where.not(id: tag.id) }
308+
it { is_expected.to be_not_found }
309+
end
310+
end
311+
end
312+
313+
# https://github.com/cerebris/jsonapi-resources/issues/1081
314+
describe 'when nullifying the taggable', skip: 'Broken upstream' do
315+
let(:new_taggable) { nil }
316+
let(:json) { '{ "data": null }' }
317+
318+
context 'unauthorized for remove_to_one_relationship' do
319+
before { disallow_operation('remove_to_one_relationship', tag, :taggable) }
320+
it { is_expected.to be_forbidden }
321+
end
322+
323+
context 'authorized for remove_to_one_relationship' do
324+
before { allow_operation('remove_to_one_relationship', tag, :taggable) }
325+
it { is_expected.to be_successful }
326+
327+
context 'limited by policy scope on taggable', skip: 'DISCUSS' do
328+
let(:policy_scope) { Article.where.not(id: tag.taggable.id) }
329+
it { is_expected.to be_not_found }
330+
end
331+
332+
# If this happens in real life, it's mostly a bug. We want to document the
333+
# behaviour in that case anyway, as it might be surprising.
334+
context 'limited by policy scope on tag' do
335+
let(:tag_policy_scope) { Tag.where.not(id: tag.id) }
336+
it { is_expected.to be_not_found }
337+
end
338+
end
339+
end
340+
end
341+
262342
describe 'DELETE /articles/:id/relationships/comments' do
263343
let(:article) { articles(:article_with_comments) }
264344
let(:comments_to_remove) { article.comments.limit(2) }

0 commit comments

Comments
 (0)