Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/jsonapi/authorization/authorizing_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def authorize_replace_fields

def authorize_create_resource
source_class = resource_klass._model_class
authorizer.create_resource(source_class, related_models)
authorizer.create_resource(source_class, related_models_with_context)
end

def authorize_remove_resource
Expand Down Expand Up @@ -277,7 +277,7 @@ def related_models_with_context
resource_class.find_by_keys(assoc_value, context: context).map(&:_model)
else
resource_class = resource_class_for_relationship(assoc_name)
resource_class.find_by_key(assoc_value[:id], context: context)._model
resource_class.find_by_key(assoc_value, context: context)._model
end

{
Expand Down
27 changes: 20 additions & 7 deletions lib/jsonapi/authorization/default_pundit_authorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,27 @@ def replace_fields(source_record, related_records_with_context)
# ==== Parameters
#
# * +source_class+ - The class of the record to be created
# * +related_records+ - An array of records to be associated to the new
# record. This will contain the records specified in the
# "relationships" key in the request
def create_resource(source_class, related_records)
# * +related_records_with_context+ - A has with the association type,
# the relationship name, and an Array of new related records.
def create_resource(source_class, related_records_with_context)
::Pundit.authorize(user, source_class, 'create?')

related_records.each do |record|
::Pundit.authorize(user, record, 'update?')
related_records_with_context.each do |data|
relation_name = data[:relation_name]
records = data[:records]
relationship_method = "create_with_#{relation_name}?"
policy = ::Pundit.policy(user, source_class)
if policy.respond_to?(relationship_method)
unless policy.public_send(relationship_method, records)
raise ::Pundit::NotAuthorizedError,
query: relationship_method,
record: source_class,
policy: policy
end
else
records.each do |record|
::Pundit.authorize(user, record, 'update?')
end
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions spec/dummy/app/policies/article_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def destroy?
raise NotImplementedError
end

def create_with_comments?(_comments)
raise NotImplementedError
end

def add_to_comments?(_comments)
raise NotImplementedError
end
Expand Down
79 changes: 45 additions & 34 deletions spec/jsonapi/authorization/default_pundit_authorizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,55 +213,66 @@

describe '#create_resource' do
let(:related_records) { Array.new(3) { Comment.new } }
let(:related_records_with_context) do
[{
relation_name: :comments,
relation_type: :to_many,
records: related_records
}]
end
let(:source_class) { source_record.class }
subject(:method_call) do
-> { authorizer.create_resource(source_class, related_records) }
-> { authorizer.create_resource(source_class, related_records_with_context) }
end

context 'authorized for create? on source class' do
before { allow_action(source_class, 'create?') }

context 'related records is empty' do
let(:related_records) { [] }
it { is_expected.not_to raise_error }
end
context 'authorized for create? on source class and related records is empty' do
before { stub_policy_actions(source_class, create?: true) }
let(:related_records) { [] }
it { is_expected.not_to raise_error }
end

context 'authorized for update? on all of the related records' do
before { related_records.each { |r| allow_action(r, 'update?') } }
it { is_expected.not_to raise_error }
end
context 'authorized for create? and authorized for create_with_<type>? on source class' do
before { stub_policy_actions(source_class, create_with_comments?: true, create?: true) }
it { is_expected.not_to raise_error }
end

context 'unauthorized for update? on any of the related records' do
let(:related_records) { [Comment.new(id: 1), Comment.new(id: 2)] }
before do
allow_action(related_records.first, 'update?')
disallow_action(related_records.last, 'update?')
end
context 'authorized for create? and unauthorized for create_with_<type>? on source class' do
let(:related_records) { [Comment.new(id: 1), Comment.new(id: 2)] }
before { stub_policy_actions(source_class, create_with_comments?: false, create?: true) }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end

it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end
context 'unauthorized for create? on source class and related records is empty' do
let(:related_records) { [] }
before { stub_policy_actions(source_class, create?: false) }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end

context 'unauthorized for create? on source class' do
before { disallow_action(source_class, 'create?') }
context 'unauthorized for create? and authorized for create_with_comments? on source class' do
before { stub_policy_actions(source_class, create_with_comments?: true, create?: false) }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end

context 'related records is empty' do
let(:related_records) { [] }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end
context 'unauthorized for create? and unauthorized for create_with_comments? on source class' do
let(:related_records) { [Comment.new(id: 1), Comment.new(id: 2)] }
before { stub_policy_actions(source_class, create_with_comments?: false, create?: false) }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end

context 'authorized for update? on all of the related records' do
before { related_records.each { |r| allow_action(r, 'update?') } }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
context 'authorized for create? where create_with_<type>? is undefined' do
context 'authorized for update? on related records' do
before do
stub_policy_actions(source_class, create?: true)
related_records.each { |r| stub_policy_actions(r, update?: true) }
end
it { is_expected.not_to raise_error }
end

context 'unauthorized for update? on any of the related records' do
let(:related_records) { [Comment.new(id: 1), Comment.new(id: 2)] }
context 'unauthorized for update? on any related records' do
before do
allow_action(related_records.first, 'update?')
disallow_action(related_records.last, 'update?')
stub_policy_actions(source_class, create?: true)
stub_policy_actions(related_records.first, update?: false)
end

it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end
end
Expand Down
25 changes: 24 additions & 1 deletion spec/requests/included_resources_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

let(:comments_policy_scope) { Comment.none }
let(:article_policy_scope) { Article.all }
let(:user_policy_scope) { User.all }

before do
allow_any_instance_of(ArticlePolicy::Scope).to receive(:resolve).and_return(
Expand All @@ -17,6 +18,9 @@
allow_any_instance_of(CommentPolicy::Scope).to receive(:resolve).and_return(
comments_policy_scope
)
allow_any_instance_of(UserPolicy::Scope).to receive(:resolve).and_return(
user_policy_scope
)
end

before do
Expand Down Expand Up @@ -266,6 +270,25 @@
let(:existing_comments) do
Array.new(2) { Comment.create }
end
let(:related_records_with_context) do
[
{
relation_type: :to_one,
relation_name: :author,
records: existing_author
},
{
relation_type: :to_many,
relation_name: :comments,
# Relax the constraints of expected records here. Lower level tests modify the
# available policy scope for comments, so we will get a different amount of records deep
# down in the other specs.
#
# This is fine, because we test resource create relationships with specific matcher
records: kind_of(Array)
}
]
end

let(:attributes_json) { '{}' }
let(:json) do
Expand Down Expand Up @@ -296,7 +319,7 @@

subject(:last_response) { post("/articles?include=#{include_query}", json) }
let!(:chained_authorizer) do
allow_operation('create_resource', Article, [existing_author, *existing_comments])
allow_operation('create_resource', Article, related_records_with_context)
end

include_examples :include_directive_tests
Expand Down
44 changes: 32 additions & 12 deletions spec/requests/tricky_operations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,24 @@
}
EOS
end
let(:related_records_with_context) do
[{
relation_name: :article,
relation_type: :to_one,
records: article
}]
end

context 'authorized for create_resource on Comment and [article]' do
context 'authorized for create_resource on Comment and newly associated article' do
let(:policy_scope) { Article.where(id: article.id) }
before { allow_operation('create_resource', Comment, [article]) }
before { allow_operation('create_resource', Comment, related_records_with_context) }

it { is_expected.to be_successful }
end

context 'unauthorized for create_resource on Comment and [article]' do
context 'unauthorized for create_resource on Comment and newly associated article' do
let(:policy_scope) { Article.where(id: article.id) }
before { disallow_operation('create_resource', Comment, [article]) }
before { disallow_operation('create_resource', Comment, related_records_with_context) }

it { is_expected.to be_forbidden }
end
Expand All @@ -73,16 +80,24 @@
EOS
end

context 'authorized for create_resource on Tag and [article]' do
let(:related_records_with_context) do
[{
relation_name: :taggable,
relation_type: :to_one,
records: article
}]
end

context 'authorized for create_resource on Tag and newly associated article' do
let(:policy_scope) { Article.where(id: article.id) }
before { allow_operation('create_resource', Tag, [article]) }
before { allow_operation('create_resource', Tag, related_records_with_context) }

it { is_expected.to be_successful }
end

context 'unauthorized for create_resource on Tag and [article]' do
context 'unauthorized for create_resource on Tag and newly associated article' do
let(:policy_scope) { Article.where(id: article.id) }
before { disallow_operation('create_resource', Tag, [article]) }
before { disallow_operation('create_resource', Tag, related_records_with_context) }

it { is_expected.to be_forbidden }
end
Expand Down Expand Up @@ -133,12 +148,17 @@

context 'limited by Comments policy scope' do
let(:comments_policy_scope) { Comment.where("id NOT IN (?)", new_comments.map(&:id)) }
let(:related_records_with_context) do
[{
relation_name: :comments,
relation_type: :to_many,
# Empty array of records as they were filtered out by the policy scope
records: []
}]
end
before { allow_operation('replace_fields', article, related_records_with_context) }

it do
pending 'DISCUSS: Should this error out somehow?'
is_expected.to be_not_found
end
it { is_expected.to be_successful }
end
end

Expand Down