# frozen_string_literal: true require 'spec_helper' RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do include GraphqlHelpers include Graphql::ResolverFactories let_it_be(:user) { create(:user) } let(:permission_single) { :foo } let(:permission_collection) { [:foo, :bar] } let(:test_object) { double(name: 'My name') } let(:authorizing_object) { test_object } # to override when combining permissions let(:permission_object_one) { authorizing_object } let(:permission_object_two) { authorizing_object } let(:query_string) { '{ item { name } }' } let(:result) do schema = empty_schema execute_query(query_type, schema: schema) end subject { result.dig('data', 'item') } shared_examples 'authorization with a single permission' do it 'returns the protected field when user has permission' do permit(permission_single) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user is not authorized' do expect(subject).to be_nil end end shared_examples 'authorization with a collection of permissions' do it 'returns the protected field when user has all permissions' do permit_on(permission_object_one, permission_collection.first) permit_on(permission_object_two, permission_collection.second) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user only has one of the permissions' do permit_on(permission_object_one, permission_collection.first) expect(subject).to be_nil end it 'returns nil when user only has the other of the permissions' do permit_on(permission_object_two, permission_collection.second) expect(subject).to be_nil end it 'returns nil when user has neither of the required permissions' do expect(subject).to be_nil end end before do # By default, disallow all permissions. allow(Ability).to receive(:allowed?).and_return(false) end describe 'Field authorizations' do let(:type) { type_factory } let(:authorizing_object) { nil } describe 'with a single permission' do let(:query_type) do query_factory do |query| query.field :item, type, null: true, resolver: new_resolver(test_object), authorize: permission_single end end include_examples 'authorization with a single permission' end describe 'with a collection of permissions' do let(:query_type) do permissions = permission_collection query_factory do |qt| qt.field :item, type, null: true, resolver: new_resolver(test_object), authorize: permissions end end include_examples 'authorization with a collection of permissions' end end describe 'Field authorizations when field is a built in type' do let(:query_type) do query_factory do |query| query.field :item, type, null: true, resolver: new_resolver(test_object) end end describe 'with a single permission' do let(:type) do type_factory do |type| type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single end end it 'returns the protected field when user has permission' do permit(permission_single) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user is not authorized' do expect(subject).to eq('name' => nil) end end describe 'with a collection of permissions' do let(:type) do permissions = permission_collection type_factory do |type| type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permissions end end it 'returns the protected field when user has all permissions' do permit(*permission_collection) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user only has one of the permissions' do permit(permission_collection.first) expect(subject).to eq('name' => nil) end it 'returns nil when user only has none of the permissions' do expect(subject).to eq('name' => nil) end end end describe 'Type authorizations' do let(:query_type) do query_factory do |query| query.field :item, type, null: true, resolver: new_resolver(test_object) end end describe 'with a single permission' do let(:type) do type_factory do |type| type.authorize permission_single end end include_examples 'authorization with a single permission' end describe 'with a collection of permissions' do let(:type) do type_factory do |type| type.authorize permission_collection end end include_examples 'authorization with a collection of permissions' end end describe 'type and field authorizations together' do let(:authorizing_object) { anything } let(:permission_1) { permission_collection.first } let(:permission_2) { permission_collection.last } let(:type) do type_factory do |type| type.authorize permission_1 end end let(:query_type) do query_factory do |query| query.field :item, type, null: true, resolver: new_resolver(test_object), authorize: permission_2 end end include_examples 'authorization with a collection of permissions' end describe 'resolver and field authorizations together' do let(:permission_1) { permission_collection.first } let(:permission_2) { permission_collection.last } let(:type) { type_factory } let(:query_type) do query_factory do |query| query.field :item, type, null: true, resolver: resolver, authorize: permission_2 end end context 'when the resolver authorizes the object' do let(:permission_object_one) { be_nil } let(:permission_object_two) { be_nil } let(:resolver) do resolver = simple_resolver(test_object) resolver.include(::Gitlab::Graphql::Authorize::AuthorizeResource) resolver.authorize permission_1 resolver.authorizes_object! resolver end include_examples 'authorization with a collection of permissions' end context 'when the resolver does not authorize the object, but instead calls authorized_find!' do let(:permission_object_one) { test_object } let(:permission_object_two) { be_nil } let(:resolver) do resolver = new_resolver(test_object, method: :find_object) resolver.authorize permission_1 resolver end include_examples 'authorization with a collection of permissions' end context 'when the resolver calls authorized_find!, but does not list any permissions' do let(:permission_object_two) { be_nil } let(:resolver) do resolver = new_resolver(test_object, method: :find_object) resolver end it 'raises a configuration error' do permit_on(permission_object_two, permission_collection.second) expect { execute_query(query_type) } .to raise_error(::Gitlab::Graphql::Authorize::AuthorizeResource::ConfigurationError) end end end describe 'when type authorizations when applied to a relay connection' do let(:query_string) { '{ item { edges { node { name } } } }' } let(:second_test_object) { double(name: 'Second thing') } let(:type) do type_factory do |type| type.authorize permission_single end end let(:query_type) do query_factory do |query| query.field :item, type.connection_type, null: true, resolver: new_resolver([test_object, second_test_object]) end end subject { result.dig('data', 'item', 'edges') } it 'returns only the elements visible to the user' do permit(permission_single) expect(subject.size).to eq 1 expect(subject.first['node']).to eq('name' => test_object.name) end it 'returns nil when user is not authorized' do expect(subject).to be_empty end describe 'limiting connections with multiple objects' do let(:query_type) do query_factory do |query| query.field :item, type.connection_type, null: true, resolver: new_resolver([test_object, second_test_object]) end end let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' } it 'only checks permissions for the first object' do expect(Ability) .to receive(:allowed?) .with(user, permission_single, test_object) .and_return(true) expect(Ability) .not_to receive(:allowed?).with(user, permission_single, second_test_object) expect(subject.size).to eq(1) end end end describe 'type authorizations when applied to a basic connection' do let(:type) do type_factory do |type| type.authorize permission_single end end let(:query_type) do query_factory do |query| query.field :item, [type], null: true, resolver: new_resolver([test_object]) end end subject { result.dig('data', 'item', 0) } include_examples 'authorization with a single permission' end describe 'Authorizations on active record relations' do let!(:visible_project) { create(:project, :private) } let!(:other_project) { create(:project, :private) } let!(:visible_issues) { create_list(:issue, 2, project: visible_project) } let!(:other_issues) { create_list(:issue, 2, project: other_project) } let!(:user) { visible_project.owner } let(:issue_type) do type_factory do |type| type.graphql_name 'FakeIssueType' type.authorize :read_issue type.field :id, GraphQL::ID_TYPE, null: false end end let(:project_type) do |type| issues = Issue.where(project: [visible_project, other_project]).order(id: :asc) type_factory do |type| type.graphql_name 'FakeProjectType' type.field :test_issues, issue_type.connection_type, null: false, resolver: new_resolver(issues) end end let(:query_type) do query_factory do |query| query.field :test_project, project_type, null: false, resolver: new_resolver(visible_project) end end let(:query_string) do <<~QRY { testProject { testIssues(first: 3) { edges { node { id } } } } } QRY end before do allow(Ability).to receive(:allowed?).and_call_original end it 'renders the issues the user has access to' do issue_edges = result.dig('data', 'testProject', 'testIssues', 'edges') issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') } expect(issue_edges.size).to eq(visible_issues.size) expect(issue_ids).to eq(visible_issues.map { |i| i.to_global_id.to_s }) end it 'does not check access on fields that will not be rendered' do expect(Ability).not_to receive(:allowed?).with(user, :read_issue, other_issues.last) result end end describe 'Authorization on GraphQL::Execution::Execute::SKIP' do let(:type) do type_factory do |type| type.authorize permission_single end end let(:query_type) do query_factory do |query| query.field :item, [type], null: true, resolver: new_resolver(GraphQL::Execution::Execute::SKIP) end end it 'skips redaction' do expect(Ability).not_to receive(:allowed?) result end end private def permit(*permissions) permit_on(authorizing_object, *permissions) end def permit_on(object, *permissions) permissions.each do |permission| allow(Ability).to receive(:allowed?).with(user, permission, object).and_return(true) end end end