diff --git a/lib/jsonapi/authorization/authorizing_processor.rb b/lib/jsonapi/authorization/authorizing_processor.rb index 7bedefb2..acaaa324 100644 --- a/lib/jsonapi/authorization/authorizing_processor.rb +++ b/lib/jsonapi/authorization/authorizing_processor.rb @@ -107,13 +107,11 @@ def authorize_replace_fields params[:resource_id], context: context )._model - - authorizer.replace_fields(source_record, related_models) + authorizer.replace_fields(source_record, related_models_with_context) end def authorize_create_resource - source_class = @resource_klass._model_class - + source_class = resource_klass._model_class authorizer.create_resource(source_class, related_models) end @@ -261,6 +259,34 @@ def related_models end end + def related_models_with_context + data = params[:data] + return { relationship: nil, relation_name: nil, records: nil } if data.nil? + + [:to_one, :to_many].flat_map do |rel_type| + data[rel_type].flat_map do |assoc_name, assoc_value| + related_models = + case assoc_value + when Hash # polymorphic relationship + resource_class = @resource_klass.resource_for(assoc_value[:type].to_s) + resource_class.find_by_key(assoc_value[:id], context: context)._model + when Array + resource_class = resource_class_for_relationship(assoc_name) + 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 + end + + { + relation_type: rel_type, + relation_name: assoc_name, + records: related_models + } + end + end + end + def authorize_model_includes(source_record) if params[:include_directives] params[:include_directives].model_includes.each do |include_item| diff --git a/lib/jsonapi/authorization/default_pundit_authorizer.rb b/lib/jsonapi/authorization/default_pundit_authorizer.rb index 05863aa1..d2c6baba 100644 --- a/lib/jsonapi/authorization/default_pundit_authorizer.rb +++ b/lib/jsonapi/authorization/default_pundit_authorizer.rb @@ -86,17 +86,11 @@ def show_related_resources(source_record) # ==== Parameters # # * +source_record+ - The record to be modified - # * +new_related_records+ - An array of records to be associated to the - # +source_record+. This will contain the records specified in the - # "relationships" key in the request - #-- - # TODO: Should probably take old records as well - def replace_fields(source_record, new_related_records) + # * +related_records_with_context+ - A hash with the association type, + # the relationship name, an Array of new related records. + def replace_fields(source_record, related_records_with_context) ::Pundit.authorize(user, source_record, 'update?') - - new_related_records.each do |record| - ::Pundit.authorize(user, record, 'update?') - end + authorize_related_records(source_record, related_records_with_context) end # POST /resources @@ -245,6 +239,20 @@ def authorize_relationship_operation(source_record, relationship_method, *args) ::Pundit.authorize(user, source_record, 'update?') end end + + def authorize_related_records(source_record, related_records_with_context) + related_records_with_context.each do |data| + relation_type = data[:relation_type] + relation_name = data[:relation_name] + records = data[:records] + case relation_type + when :to_many + replace_to_many_relationship(source_record, records, relation_name) + when :to_one + replace_to_one_relationship(source_record, records, relation_name) + end + end + end end end end diff --git a/spec/jsonapi/authorization/default_pundit_authorizer_spec.rb b/spec/jsonapi/authorization/default_pundit_authorizer_spec.rb index 2b99494d..63599ab5 100644 --- a/spec/jsonapi/authorization/default_pundit_authorizer_spec.rb +++ b/spec/jsonapi/authorization/default_pundit_authorizer_spec.rb @@ -161,8 +161,15 @@ describe '#replace_fields' 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 subject(:method_call) do - -> { authorizer.replace_fields(source_record, related_records) } + -> { authorizer.replace_fields(source_record, related_records_with_context) } end context 'authorized for update? on source record' do @@ -173,18 +180,13 @@ 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 } + context 'authorized for replace_comments? on source record' do + before { stub_policy_actions(source_record, replace_comments?: true, update?: true) } + it { is_expected.not_to raise_error(::Pundit::NotAuthorizedError) } 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 'unauthorized for replace_comments? on source record' do + before { stub_policy_actions(source_record, replace_comments?: false, update?: true) } it { is_expected.to raise_error(::Pundit::NotAuthorizedError) } end end @@ -197,18 +199,13 @@ 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?') } } + context 'authorized for replace_comments? on source record' do + before { stub_policy_actions(source_record, replace_comments?: true, update?: false) } it { is_expected.to raise_error(::Pundit::NotAuthorizedError) } 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 'unauthorized for replace_comments? on source record' do + before { stub_policy_actions(source_record, replace_comments?: false, update?: false) } it { is_expected.to raise_error(::Pundit::NotAuthorizedError) } end end diff --git a/spec/requests/tricky_operations_spec.rb b/spec/requests/tricky_operations_spec.rb index 19bc353f..9d6dd876 100644 --- a/spec/requests/tricky_operations_spec.rb +++ b/spec/requests/tricky_operations_spec.rb @@ -92,6 +92,13 @@ let!(:new_comments) do Array.new(2) { Comment.create } end + let(:related_records_with_context) do + [{ + relation_name: :comments, + relation_type: :to_many, + records: new_comments + }] + end let(:policy_scope) { Article.where(id: article.id) } let(:comments_policy_scope) { Comment.all } before do @@ -120,13 +127,13 @@ context 'authorized for replace_fields on article and all new records' do context 'not limited by Comments policy scope' do - before { allow_operation('replace_fields', article, new_comments) } + before { allow_operation('replace_fields', article, related_records_with_context) } it { is_expected.to be_successful } end context 'limited by Comments policy scope' do let(:comments_policy_scope) { Comment.where("id NOT IN (?)", new_comments.map(&:id)) } - before { allow_operation('replace_fields', article, new_comments) } + before { allow_operation('replace_fields', article, related_records_with_context) } it do pending 'DISCUSS: Should this error out somehow?' @@ -136,7 +143,7 @@ end context 'unauthorized for replace_fields on article and all new records' do - before { disallow_operation('replace_fields', article, new_comments) } + before { disallow_operation('replace_fields', article, related_records_with_context) } it { is_expected.to be_forbidden } end