Commit 7af1ba12 authored by Nick Thomas's avatar Nick Thomas

Merge branch '54417-graphql-type-authorization' into 'master'

GraphQL Type authorization

Closes #54417

See merge request gitlab-org/gitlab-ce!25724
parents 60a0ef21 8cf0d892
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
module Types module Types
module Ci module Ci
class PipelineType < BaseObject class PipelineType < BaseObject
expose_permissions Types::PermissionTypes::Ci::Pipeline
graphql_name 'Pipeline' graphql_name 'Pipeline'
authorize :read_pipeline
expose_permissions Types::PermissionTypes::Ci::Pipeline
field :id, GraphQL::ID_TYPE, null: false field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false field :iid, GraphQL::ID_TYPE, null: false
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
module Types module Types
class IssueType < BaseObject class IssueType < BaseObject
expose_permissions Types::PermissionTypes::Issue
graphql_name 'Issue' graphql_name 'Issue'
authorize :read_issue
expose_permissions Types::PermissionTypes::Issue
present_using IssuePresenter present_using IssuePresenter
field :iid, GraphQL::ID_TYPE, null: false field :iid, GraphQL::ID_TYPE, null: false
...@@ -15,16 +17,14 @@ module Types ...@@ -15,16 +17,14 @@ module Types
field :author, Types::UserType, field :author, Types::UserType,
null: false, null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }, resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
authorize: :read_user
field :assignees, Types::UserType.connection_type, null: true field :assignees, Types::UserType.connection_type, null: true
field :labels, Types::LabelType.connection_type, null: true field :labels, Types::LabelType.connection_type, null: true
field :milestone, Types::MilestoneType, field :milestone, Types::MilestoneType,
null: true, null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }, resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
authorize: :read_milestone
field :due_date, Types::TimeType, null: true field :due_date, Types::TimeType, null: true
field :confidential, GraphQL::BOOLEAN_TYPE, null: false field :confidential, GraphQL::BOOLEAN_TYPE, null: false
......
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
module Types module Types
class MergeRequestType < BaseObject class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
authorize :read_merge_request
expose_permissions Types::PermissionTypes::MergeRequest expose_permissions Types::PermissionTypes::MergeRequest
present_using MergeRequestPresenter present_using MergeRequestPresenter
graphql_name 'MergeRequest'
field :id, GraphQL::ID_TYPE, null: false field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false field :title, GraphQL::STRING_TYPE, null: false
...@@ -48,7 +50,7 @@ module Types ...@@ -48,7 +50,7 @@ module Types
field :downvotes, GraphQL::INT_TYPE, null: false field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline, authorize: :read_pipeline field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline
field :pipelines, Types::Ci::PipelineType.connection_type, field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver resolver: Resolvers::MergeRequestPipelinesResolver
end end
......
...@@ -4,6 +4,8 @@ module Types ...@@ -4,6 +4,8 @@ module Types
class MilestoneType < BaseObject class MilestoneType < BaseObject
graphql_name 'Milestone' graphql_name 'Milestone'
authorize :read_milestone
field :description, GraphQL::STRING_TYPE, null: true field :description, GraphQL::STRING_TYPE, null: true
field :title, GraphQL::STRING_TYPE, null: false field :title, GraphQL::STRING_TYPE, null: false
field :state, GraphQL::STRING_TYPE, null: false field :state, GraphQL::STRING_TYPE, null: false
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
module Types module Types
class ProjectType < BaseObject class ProjectType < BaseObject
expose_permissions Types::PermissionTypes::Project
graphql_name 'Project' graphql_name 'Project'
authorize :read_project
expose_permissions Types::PermissionTypes::Project
field :id, GraphQL::ID_TYPE, null: false field :id, GraphQL::ID_TYPE, null: false
field :full_path, GraphQL::ID_TYPE, null: false field :full_path, GraphQL::ID_TYPE, null: false
...@@ -67,14 +69,12 @@ module Types ...@@ -67,14 +69,12 @@ module Types
field :merge_requests, field :merge_requests,
Types::MergeRequestType.connection_type, Types::MergeRequestType.connection_type,
null: true, null: true,
resolver: Resolvers::MergeRequestsResolver, resolver: Resolvers::MergeRequestsResolver
authorize: :read_merge_request
field :merge_request, field :merge_request,
Types::MergeRequestType, Types::MergeRequestType,
null: true, null: true,
resolver: Resolvers::MergeRequestsResolver.single, resolver: Resolvers::MergeRequestsResolver.single
authorize: :read_merge_request
field :issues, field :issues,
Types::IssueType.connection_type, Types::IssueType.connection_type,
...@@ -88,7 +88,7 @@ module Types ...@@ -88,7 +88,7 @@ module Types
field :pipelines, field :pipelines,
Types::Ci::PipelineType.connection_type, Types::Ci::PipelineType.connection_type,
null: false, null: true,
resolver: Resolvers::ProjectPipelinesResolver resolver: Resolvers::ProjectPipelinesResolver
end end
end end
...@@ -7,8 +7,7 @@ module Types ...@@ -7,8 +7,7 @@ module Types
field :project, Types::ProjectType, field :project, Types::ProjectType,
null: true, null: true,
resolver: Resolvers::ProjectResolver, resolver: Resolvers::ProjectResolver,
description: "Find a project", description: "Find a project"
authorize: :read_project
field :metadata, Types::MetadataType, field :metadata, Types::MetadataType,
null: true, null: true,
......
...@@ -4,6 +4,8 @@ module Types ...@@ -4,6 +4,8 @@ module Types
class UserType < BaseObject class UserType < BaseObject
graphql_name 'User' graphql_name 'User'
authorize :read_user
present_using UserPresenter present_using UserPresenter
field :name, GraphQL::STRING_TYPE, null: false field :name, GraphQL::STRING_TYPE, null: false
......
---
title: GraphQL Types can be made to always authorize access to resources of that Type
merge_request: 25724
author:
type: added
# frozen_string_literal: true # frozen_string_literal: true
GraphQL::ObjectType.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
GraphQL::Field.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize)) GraphQL::Field.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
GraphQL::Schema::Object.accepts_definition(:authorize)
GraphQL::Schema::Field.accepts_definition(:authorize) GraphQL::Schema::Field.accepts_definition(:authorize)
...@@ -9,38 +9,6 @@ can be shared. ...@@ -9,38 +9,6 @@ can be shared.
It is also possible to add a `private_token` to the querystring, or It is also possible to add a `private_token` to the querystring, or
add a `HTTP_PRIVATE_TOKEN` header. add a `HTTP_PRIVATE_TOKEN` header.
### Authorization
Fields can be authorized using the same abilities used in the Rails
app. This can be done by supplying the `authorize` option:
```ruby
module Types
class QueryType < BaseObject
graphql_name 'Query'
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :read_project
end
end
```
Fields can be authorized against multiple abilities, in which case all
ability checks must pass. This requires explicitly passing a block to `field`:
```ruby
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
authorize [:read_project, :another_ability]
end
```
The object found by the resolve call is used for authorization.
TIP: **Tip:**
When authorizing collections, try to load only what the currently
authenticated user is allowed to view with our existing finders first.
This minimizes database queries and unnecessary authorization checks of
the loaded records.
## Types ## Types
When exposing a model through the GraphQL API, we do so by creating a When exposing a model through the GraphQL API, we do so by creating a
...@@ -197,6 +165,114 @@ end ...@@ -197,6 +165,114 @@ end
policies at once. The fields for these will all have be non-nullable policies at once. The fields for these will all have be non-nullable
booleans with a default description. booleans with a default description.
## Authorization
Authorizations can be applied to both types and fields using the same
abilities as in the Rails app.
If the:
- Currently authenticated user fails the authorization, the authorized
resource will be returned as `null`.
- Resource is part of a collection, the collection will be filtered to
exclude the objects that the user's authorization checks failed against.
TIP: **Tip:**
Try to load only what the currently authenticated user is allowed to
view with our existing finders first, without relying on authorization
to filter the records. This minimizes database queries and unnecessary
authorization checks of the loaded records.
### Type authorization
Authorize a type by passing an ability to the `authorize` method. All
fields with the same type will be authorized by checking that the
currently authenticated user has the required ability.
For example, the following authorization ensures that the currently
authenticated user can only see projects that they have the
`read_project` ability for (so long as the project is returned in a
field that uses `Types::ProjectType`):
```ruby
module Types
class ProjectType < BaseObject
authorize :read_project
end
end
```
You can also authorize against multiple abilities, in which case all of
the ability checks must pass.
For example, the following authorization ensures that the currently
authenticated user must have `read_project` and `another_ability`
abilities to see a project:
```ruby
module Types
class ProjectType < BaseObject
authorize [:read_project, :another_ability]
end
end
```
### Field authorization
Fields can be authorized with the `authorize` option.
For example, the following authorization ensures that the currently
authenticated user must have the `owner_access` ability to see the
project:
```ruby
module Types
class MyType < BaseObject
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access
end
end
```
Fields can also be authorized against multiple abilities, in which case
all of ability checks must pass. **Note:** This requires explicitly
passing a block to `field`:
```ruby
module Types
class MyType < BaseObject
field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
authorize [:owner_access, :another_ability]
end
end
end
```
NOTE: **Note:** If the field's type already [has a particular
authorization](#type-authorization) then there is no need to add that
same authorization to the field.
### Type and Field authorizations together
Authorizations are cumulative, so where authorizations are defined on
a field, and also on the field's type, then the currently authenticated
user would need to pass all ability checks.
In the following simplified example the currently authenticated user
would need both `first_permission` and `second_permission` abilities in
order to see the author of the issue.
```ruby
class UserType
authorize :first_permission
end
```
```ruby
class IssueType
field :author, UserType, authorize: :second_permission
end
```
## Resolvers ## Resolvers
To find objects to display in a field, we can add resolvers to To find objects to display in a field, we can add resolvers to
......
# frozen_string_literal: true
module Gitlab
module Graphql
module Authorize
class AuthorizeFieldService
def initialize(field)
@field = field
@old_resolve_proc = @field.resolve_proc
end
def authorizations?
authorizations.present?
end
def authorized_resolve
proc do |obj, args, ctx|
resolved_obj = @old_resolve_proc.call(obj, args, ctx)
checker = build_checker(ctx[:current_user])
if resolved_obj.respond_to?(:then)
resolved_obj.then(&checker)
else
checker.call(resolved_obj)
end
end
end
private
def authorizations
@authorizations ||= (type_authorizations + field_authorizations).uniq
end
# Returns any authorize metadata from the return type of @field
def type_authorizations
type = @field.type
# When the return type of @field is a collection, find the singular type
if type.get_field('edges')
type = node_type_for_relay_connection(type)
elsif type.list?
type = node_type_for_basic_connection(type)
end
Array.wrap(type.metadata[:authorize])
end
# Returns any authorize metadata from @field
def field_authorizations
Array.wrap(@field.metadata[:authorize])
end
def build_checker(current_user)
lambda do |value|
# Load the elements if they were not loaded by BatchLoader yet
value = value.sync if value.respond_to?(:sync)
check = lambda do |object|
authorizations.all? do |ability|
Ability.allowed?(current_user, ability, object)
end
end
case value
when Array, ActiveRecord::Relation
value.select(&check)
else
value if check.call(value)
end
end
end
# Returns the singular type for relay connections.
# This will be the type class of edges.node
def node_type_for_relay_connection(type)
type = type.get_field('edges').type.unwrap.get_field('node')&.type
if type.nil?
raise Gitlab::Graphql::Errors::ConnectionDefinitionError,
'Connection Type must conform to the Relay Cursor Connections Specification'
end
type
end
# Returns the singular type for basic connections, for example `[Types::ProjectType]`
def node_type_for_basic_connection(type)
type.unwrap
end
end
end
end
end
...@@ -7,46 +7,12 @@ module Gitlab ...@@ -7,46 +7,12 @@ module Gitlab
# Replace the resolver for the field with one that will only return the # Replace the resolver for the field with one that will only return the
# resolved object if the permissions check is successful. # resolved object if the permissions check is successful.
def instrument(_type, field) def instrument(_type, field)
required_permissions = Array.wrap(field.metadata[:authorize]) service = AuthorizeFieldService.new(field)
return field if required_permissions.empty?
old_resolver = field.resolve_proc if service.authorizations?
field.redefine { resolve(service.authorized_resolve) }
new_resolver = -> (obj, args, ctx) do else
resolved_obj = old_resolver.call(obj, args, ctx) field
checker = build_checker(ctx[:current_user], required_permissions)
if resolved_obj.respond_to?(:then)
resolved_obj.then(&checker)
else
checker.call(resolved_obj)
end
end
field.redefine do
resolve(new_resolver)
end
end
private
def build_checker(current_user, abilities)
lambda do |value|
# Load the elements if they weren't loaded by BatchLoader yet
value = value.sync if value.respond_to?(:sync)
check = lambda do |object|
abilities.all? do |ability|
Ability.allowed?(current_user, ability, object)
end
end
case value
when Array
value.select(&check)
else
value if check.call(value)
end
end end
end end
end end
......
...@@ -6,6 +6,7 @@ module Gitlab ...@@ -6,6 +6,7 @@ module Gitlab
BaseError = Class.new(GraphQL::ExecutionError) BaseError = Class.new(GraphQL::ExecutionError)
ArgumentError = Class.new(BaseError) ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError) ResourceNotAvailable = Class.new(BaseError)
ConnectionDefinitionError = Class.new(BaseError)
end end
end end
end end
...@@ -5,61 +5,192 @@ require 'spec_helper' ...@@ -5,61 +5,192 @@ require 'spec_helper'
describe 'Gitlab::Graphql::Authorization' do describe 'Gitlab::Graphql::Authorization' do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:permission_single) { :foo }
let(:permission_collection) { [:foo, :bar] }
let(:test_object) { double(name: 'My name') } let(:test_object) { double(name: 'My name') }
let(:object_type) { object_type_class } let(:query_string) { '{ object() { name } }' }
let(:query_type) { query_type_class(object_type, test_object) } let(:result) { execute_query(query_type)['data'] }
let(:schema) { schema_class(query_type) }
let(:execute) do subject { result['object'] }
schema.execute(
query_string, shared_examples 'authorization with a single permission' do
context: { current_user: user }, it 'returns the protected field when user has permission' do
variables: {} 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 end
let(:result) { execute['data'] } shared_examples 'authorization with a collection of permissions' do
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 be_nil
end
it 'returns nil when user only has none of the permissions' do
expect(subject).to be_nil
end
end
before do before do
# By default, disallow all permissions. # By default, disallow all permissions.
allow(Ability).to receive(:allowed?).and_return(false) allow(Ability).to receive(:allowed?).and_return(false)
end end
describe 'authorizing with a single permission' do describe 'Field authorizations' do
let(:query_string) { '{ singlePermission() { name } }' } let(:type) { type_factory }
subject { result['singlePermission'] } describe 'with a single permission' do
let(:query_type) do
query_factory do |query|
query.field :object, type, null: true, resolve: ->(obj, args, ctx) { 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 :object, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
authorize permissions
end
end
end
it 'should return the protected field when user has permission' do include_examples 'authorization with a collection of permissions'
permit(:foo) end
end
expect(subject['name']).to eq(test_object.name) describe 'Type authorizations' do
let(:query_type) do
query_factory do |query|
query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
end
end end
it 'should return nil when user is not authorized' do describe 'with a single permission' do
expect(subject).to be_nil 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
end end
describe 'authorizing with an Array of permissions' do describe 'type and field authorizations together' do
let(:query_string) { '{ permissionCollection() { name } }' } let(:permission_1) { permission_collection.first }
let(:permission_2) { permission_collection.last }
subject { result['permissionCollection'] } let(:type) do
type_factory do |type|
type.authorize permission_1
end
end
it 'should return the protected field when user has all permissions' do let(:query_type) do
permit(:foo, :bar) query_factory do |query|
query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
end
end
expect(subject['name']).to eq(test_object.name) include_examples 'authorization with a collection of permissions'
end
describe 'type authorizations when applied to a relay connection' do
let(:query_string) { '{ object() { edges { node { name } } } }' }
let(:type) do
type_factory do |type|
type.authorize permission_single
end
end
let(:query_type) do
query_factory do |query|
query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object] }
end
end end
it 'should return nil when user only has one of the permissions' do subject { result.dig('object', 'edges') }
permit(:foo)
expect(subject).to be_nil it 'returns the protected field when user has permission' do
permit(permission_single)
expect(subject).not_to be_empty
expect(subject.first['node']).to eq('name' => test_object.name)
end end
it 'should return nil when user only has none of the permissions' do it 'returns nil when user is not authorized' do
expect(subject).to be_nil expect(subject).to be_empty
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 :object, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
end
end
subject { result['object'].first }
include_examples 'authorization with a single permission'
end
describe 'when connections do not follow the correct specification' do
let(:query_string) { '{ object() { edges { node { name }} } }' }
let(:type) do
bad_node = type_factory do |type|
type.graphql_name 'BadNode'
type.field :bad_node, GraphQL::STRING_TYPE, null: true
end
type_factory do |type|
type.field :edges, [bad_node], null: true
end
end
let(:query_type) do
query_factory do |query|
query.field :object, type, null: true
end
end
it 'throws an error' do
expect { result }.to raise_error(Gitlab::Graphql::Errors::ConnectionDefinitionError)
end end
end end
...@@ -71,36 +202,34 @@ describe 'Gitlab::Graphql::Authorization' do ...@@ -71,36 +202,34 @@ describe 'Gitlab::Graphql::Authorization' do
end end
end end
def object_type_class def type_factory
Class.new(Types::BaseObject) do Class.new(Types::BaseObject) do
graphql_name 'TestObject' graphql_name 'TestType'
field :name, GraphQL::STRING_TYPE, null: true field :name, GraphQL::STRING_TYPE, null: true
yield(self) if block_given?
end end
end end
def query_type_class(type, object) def query_factory
Class.new(Types::BaseObject) do Class.new(Types::BaseObject) do
graphql_name 'TestQuery' graphql_name 'TestQuery'
field :single_permission, type, yield(self) if block_given?
null: true,
authorize: :foo,
resolve: ->(obj, args, ctx) { object }
field :permission_collection, type,
null: true,
resolve: ->(obj, args, ctx) { object } do
authorize [:foo, :bar]
end
end end
end end
def schema_class(query) def execute_query(query_type)
Class.new(GraphQL::Schema) do schema = Class.new(GraphQL::Schema) do
use Gitlab::Graphql::Authorize use Gitlab::Graphql::Authorize
query(query_type)
query(query)
end end
schema.execute(
query_string,
context: { current_user: user },
variables: {}
)
end end
end end
...@@ -4,4 +4,6 @@ describe GitlabSchema.types['Issue'] do ...@@ -4,4 +4,6 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) } it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
it { expect(described_class.graphql_name).to eq('Issue') } it { expect(described_class.graphql_name).to eq('Issue') }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
end end
...@@ -3,14 +3,9 @@ require 'spec_helper' ...@@ -3,14 +3,9 @@ require 'spec_helper'
describe GitlabSchema.types['MergeRequest'] do describe GitlabSchema.types['MergeRequest'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) } it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
describe 'head pipeline' do it { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
it 'has a head pipeline field' do
expect(described_class).to have_graphql_field(:head_pipeline)
end
it 'authorizes the field' do describe 'nested head pipeline' do
expect(described_class.fields['headPipeline']) it { expect(described_class).to have_graphql_field(:head_pipeline) }
.to require_graphql_authorizations(:read_pipeline)
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Milestone'] do
it { expect(described_class.graphql_name).to eq('Milestone') }
it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
end
...@@ -5,19 +5,11 @@ describe GitlabSchema.types['Project'] do ...@@ -5,19 +5,11 @@ describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') } it { expect(described_class.graphql_name).to eq('Project') }
it { expect(described_class).to require_graphql_authorizations(:read_project) }
describe 'nested merge request' do describe 'nested merge request' do
it { expect(described_class).to have_graphql_field(:merge_requests) } it { expect(described_class).to have_graphql_field(:merge_requests) }
it { expect(described_class).to have_graphql_field(:merge_request) } it { expect(described_class).to have_graphql_field(:merge_request) }
it 'authorizes the merge request' do
expect(described_class.fields['mergeRequest'])
.to require_graphql_authorizations(:read_merge_request)
end
it 'authorizes the merge requests' do
expect(described_class.fields['mergeRequests'])
.to require_graphql_authorizations(:read_merge_request)
end
end end
describe 'nested issues' do describe 'nested issues' do
......
...@@ -15,10 +15,6 @@ describe GitlabSchema.types['Query'] do ...@@ -15,10 +15,6 @@ describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::ProjectType) is_expected.to have_graphql_type(Types::ProjectType)
is_expected.to have_graphql_resolver(Resolvers::ProjectResolver) is_expected.to have_graphql_resolver(Resolvers::ProjectResolver)
end end
it 'authorizes with read_project' do
is_expected.to require_graphql_authorizations(:read_project)
end
end end
describe 'metadata field' do describe 'metadata field' do
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['User'] do
it { expect(described_class.graphql_name).to eq('User') }
it { expect(described_class).to require_graphql_authorizations(:read_user) }
end
...@@ -2,13 +2,17 @@ ...@@ -2,13 +2,17 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Graphql::Authorize::Instrumentation do # Also see spec/graphql/features/authorization_spec.rb for
# integration tests of AuthorizeFieldService
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
describe '#build_checker' do describe '#build_checker' do
let(:current_user) { double(:current_user) } let(:current_user) { double(:current_user) }
let(:abilities) { [double(:first_ability), double(:last_ability)] } let(:abilities) { [double(:first_ability), double(:last_ability)] }
let(:checker) do let(:checker) do
described_class.new.__send__(:build_checker, current_user, abilities) service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user)
end end
it 'returns a checker which checks for a single object' do it 'returns a checker which checks for a single object' do
...@@ -56,12 +60,14 @@ describe Gitlab::Graphql::Authorize::Instrumentation do ...@@ -56,12 +60,14 @@ describe Gitlab::Graphql::Authorize::Instrumentation do
.to contain_exactly(allowed) .to contain_exactly(allowed)
end end
end end
end
def spy_ability_check_for(ability, object, passed: true) private
expect(Ability)
.to receive(:allowed?) def spy_ability_check_for(ability, object, passed: true)
.with(current_user, ability, object) expect(Ability)
.and_return(passed) .to receive(:allowed?)
end .with(current_user, ability, object)
.and_return(passed)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment