diff options
Diffstat (limited to 'spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb')
-rw-r--r-- | spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb | 226 |
1 files changed, 153 insertions, 73 deletions
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb index 7576523ce52..c88506899cd 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb @@ -27,13 +27,17 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do end end + def resolve + service.authorized_resolve[type_instance, {}, context] + end + subject(:service) { described_class.new(field) } describe '#authorized_resolve' do let_it_be(:current_user) { build(:user) } let_it_be(:presented_object) { 'presented object' } let_it_be(:query_type) { GraphQL::ObjectType.new } - let_it_be(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} + let_it_be(:schema) { GitlabSchema } let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) } let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) } @@ -41,125 +45,201 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do let(:type_instance) { type_class.authorized_new(presented_object, context) } let(:field) { type_class.fields['testField'].to_graphql } - subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) } + subject(:resolved) { ::Gitlab::Graphql::Lazy.force(resolve) } - context 'scalar types' do - shared_examples 'checking permissions on the presented object' do - it 'checks the abilities on the object being presented and returns the value' do - expected_permissions.each do |permission| - spy_ability_check_for(permission, presented_object, passed: true) - end + context 'reading the field of a lazy value' do + let(:ability) { :read_field } + let(:presented_object) { lazy_upcase('a') } + let(:type_class) { type_with_field(GraphQL::STRING_TYPE, ability) } - expect(resolved).to eq('Resolved value') + let(:upcaser) do + Module.new do + def self.upcase(strs) + strs.map(&:upcase) + end end + end - it 'returns nil if the value was not authorized' do - allow(Ability).to receive(:allowed?).and_return false - - expect(resolved).to be_nil + def lazy_upcase(str) + ::BatchLoader::GraphQL.for(str).batch do |strs, found| + strs.zip(upcaser.upcase(strs)).each { |s, us| found[s, us] } end end - context 'when the field is a built-in scalar type' do - let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) } - let(:expected_permissions) { [:read_field] } + it 'does not run authorizations until we force the resolved value' do + expect(Ability).not_to receive(:allowed?) - it_behaves_like 'checking permissions on the presented object' + expect(resolve).to respond_to(:force) end - context 'when the field is a list of scalar types' do - let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) } - let(:expected_permissions) { [:read_field] } + it 'runs authorizations when we force the resolved value' do + spy_ability_check_for(ability, 'A') - it_behaves_like 'checking permissions on the presented object' + expect(resolved).to eq('Resolved value') end - context 'when the field is sub-classed scalar type' do - let(:type_class) { type_with_field(Types::TimeType, :read_field) } - let(:expected_permissions) { [:read_field] } + it 'redacts values that fail the permissions check' do + spy_ability_check_for(ability, 'A', passed: false) - it_behaves_like 'checking permissions on the presented object' + expect(resolved).to be_nil end - context 'when the field is a list of sub-classed scalar types' do - let(:type_class) { type_with_field([Types::TimeType], :read_field) } - let(:expected_permissions) { [:read_field] } + context 'we batch two calls' do + def resolve(value) + instance = type_class.authorized_new(lazy_upcase(value), context) + service.authorized_resolve[instance, {}, context] + end - it_behaves_like 'checking permissions on the presented object' - end - end + it 'batches resolution, but authorizes each object separately' do + expect(upcaser).to receive(:upcase).once.and_call_original + spy_ability_check_for(:read_field, 'A', passed: true) + spy_ability_check_for(:read_field, 'B', passed: false) + spy_ability_check_for(:read_field, 'C', passed: true) - context 'when the field is a connection' do - context 'when it resolves to nil' do - let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) } + a = resolve('a') + b = resolve('b') + c = resolve('c') - it 'does not fail when authorizing' do - expect(resolved).to be_nil + expect(a.force).to be_present + expect(b.force).to be_nil + expect(c.force).to be_present end end end - context 'when the field is a specific type' do - let(:custom_type) { type(:read_type) } - let(:object_in_field) { double('presented in field') } + shared_examples 'authorizing fields' do + context 'scalar types' do + shared_examples 'checking permissions on the presented object' do + it 'checks the abilities on the object being presented and returns the value' do + expected_permissions.each do |permission| + spy_ability_check_for(permission, presented_object, passed: true) + end - let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) } - let(:type_instance) { type_class.authorized_new(object_in_field, context) } + expect(resolved).to eq('Resolved value') + end - subject(:resolved) { service.authorized_resolve.call(type_instance, {}, context) } + it 'returns nil if the value was not authorized' do + allow(Ability).to receive(:allowed?).and_return false - it 'checks both field & type permissions' do - spy_ability_check_for(:read_field, object_in_field, passed: true) - spy_ability_check_for(:read_type, object_in_field, passed: true) + expect(resolved).to be_nil + end + end - expect(resolved).to eq(object_in_field) - end + context 'when the field is a built-in scalar type' do + let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) } + let(:expected_permissions) { [:read_field] } - it 'returns nil if viewing was not allowed' do - spy_ability_check_for(:read_field, object_in_field, passed: false) - spy_ability_check_for(:read_type, object_in_field, passed: true) + it_behaves_like 'checking permissions on the presented object' + end - expect(resolved).to be_nil + context 'when the field is a list of scalar types' do + let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) } + let(:expected_permissions) { [:read_field] } + + it_behaves_like 'checking permissions on the presented object' + end + + context 'when the field is sub-classed scalar type' do + let(:type_class) { type_with_field(Types::TimeType, :read_field) } + let(:expected_permissions) { [:read_field] } + + it_behaves_like 'checking permissions on the presented object' + end + + context 'when the field is a list of sub-classed scalar types' do + let(:type_class) { type_with_field([Types::TimeType], :read_field) } + let(:expected_permissions) { [:read_field] } + + it_behaves_like 'checking permissions on the presented object' + end end - context 'when the field is not nullable' do - let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) } + context 'when the field is a connection' do + context 'when it resolves to nil' do + let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) } + + it 'does not fail when authorizing' do + expect(resolved).to be_nil + end + end - it 'returns nil when viewing is not allowed' do - spy_ability_check_for(:read_type, object_in_field, passed: false) + context 'when it returns values' do + let(:objects) { [1, 2, 3] } + let(:field_type) { type([:read_object]).connection_type } + let(:type_class) { type_with_field(field_type, [], objects) } - expect(resolved).to be_nil + it 'filters out unauthorized values' do + spy_ability_check_for(:read_object, 1, passed: true) + spy_ability_check_for(:read_object, 2, passed: false) + spy_ability_check_for(:read_object, 3, passed: true) + + expect(resolved.nodes).to eq [1, 3] + end end end - context 'when the field is a list' do - let(:object_1) { double('presented in field 1') } - let(:object_2) { double('presented in field 2') } - let(:presented_types) { [double(object: object_1), double(object: object_2)] } + context 'when the field is a specific type' do + let(:custom_type) { type(:read_type) } + let(:object_in_field) { double('presented in field') } + + let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) } + let(:type_instance) { type_class.authorized_new(object_in_field, context) } + + it 'checks both field & type permissions' do + spy_ability_check_for(:read_field, object_in_field, passed: true) + spy_ability_check_for(:read_type, object_in_field, passed: true) + + expect(resolved).to eq(object_in_field) + end + + it 'returns nil if viewing was not allowed' do + spy_ability_check_for(:read_field, object_in_field, passed: false) + spy_ability_check_for(:read_type, object_in_field, passed: true) - let(:type_class) { type_with_field([custom_type], :read_field, presented_types) } - let(:type_instance) { type_class.authorized_new(presented_types, context) } + expect(resolved).to be_nil + end - it 'checks all permissions' do - allow(Ability).to receive(:allowed?) { true } + context 'when the field is not nullable' do + let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) } - spy_ability_check_for(:read_field, object_1, passed: true) - spy_ability_check_for(:read_type, object_1, passed: true) - spy_ability_check_for(:read_field, object_2, passed: true) - spy_ability_check_for(:read_type, object_2, passed: true) + it 'returns nil when viewing is not allowed' do + spy_ability_check_for(:read_type, object_in_field, passed: false) - expect(resolved).to eq(presented_types) + expect(resolved).to be_nil + end end - it 'filters out objects that the user cannot see' do - allow(Ability).to receive(:allowed?) { true } + context 'when the field is a list' do + let(:object_1) { double('presented in field 1') } + let(:object_2) { double('presented in field 2') } + let(:presented_types) { [double(object: object_1), double(object: object_2)] } + + let(:type_class) { type_with_field([custom_type], :read_field, presented_types) } + let(:type_instance) { type_class.authorized_new(presented_types, context) } + + it 'checks all permissions' do + allow(Ability).to receive(:allowed?) { true } - spy_ability_check_for(:read_type, object_1, passed: false) + spy_ability_check_for(:read_field, object_1, passed: true) + spy_ability_check_for(:read_type, object_1, passed: true) + spy_ability_check_for(:read_field, object_2, passed: true) + spy_ability_check_for(:read_type, object_2, passed: true) - expect(resolved.map(&:object)).to contain_exactly(object_2) + expect(resolved).to eq(presented_types) + end + + it 'filters out objects that the user cannot see' do + allow(Ability).to receive(:allowed?) { true } + + spy_ability_check_for(:read_type, object_1, passed: false) + + expect(resolved).to contain_exactly(have_attributes(object: object_2)) + end end end end + + it_behaves_like 'authorizing fields' end private |