From e441eae1e1027f380771a944636b7ae9636bc19a Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Thu, 18 Feb 2021 18:49:27 +0000 Subject: [PATCH 1/2] Update GraphQL authorization developer documentation This is related to https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40088 and contains the documentation updates which describe the changes to our authorization systems. --- doc/development/api_graphql_styleguide.md | 108 +-------- .../graphql_guide/authorization.md | 224 ++++++++++++++++++ doc/development/graphql_guide/index.md | 1 + 3 files changed, 226 insertions(+), 107 deletions(-) create mode 100644 doc/development/graphql_guide/authorization.md diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 7e4e7007ca25b2..d3c2e012891a81 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -772,113 +772,7 @@ argument :title, GraphQL::STRING_TYPE, ## 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 is returned as `null`. -- Resource is part of a collection, the collection is filtered to - exclude the objects that the user's authorization checks failed against. - -Also see [authorizing resources in a mutation](#authorizing-resources). - -NOTE: -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 is 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. 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 -``` - -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 -``` +See: [GraphQL Authorization](graphql_guide/authorization.md) ## Resolvers diff --git a/doc/development/graphql_guide/authorization.md b/doc/development/graphql_guide/authorization.md new file mode 100644 index 00000000000000..62cc284264b69d --- /dev/null +++ b/doc/development/graphql_guide/authorization.md @@ -0,0 +1,224 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# GraphQL Authorization + +Authorizations can be applied in these places: + +- Types: + - Objects (all classes descending from `::Types::BaseObject`) + - Enums (all classes descending from `::Types::BaseEnum`) +- Resolvers: + - Field resolvers (all classes descending from `::Types::BaseResolver`) + - Mutations (all classes descending from `::Types::BaseMutation`) +- Fields (all fields declared using the `field` DSL method) + +Authorizations cannot be specified for abstract types (interfaces and +unions). Abstract types delegate to their member types. +Basic built in scalars (such as integers) do not have authorizations. + +Our authorization system uses the same [`DeclarativePolicy`](../policies.md) +system as throughout the rest of the application. + +- For single values (such as `Query.project`), if the currently authenticated + user fails the authorization, the field resolves to `null`. +- For collections (such as `Project.issues`), the collection is filtered to + exclude the objects that the user's authorization checks failed against. This + process of filtering (also known as _redaction_) happens after pagination, so + some pages may be smaller than the requested page size, due to redacted + objects being removed. + +Also see [authorizing resources in a mutation](../api_graphql_styleguide.md#authorizing-resources). + +NOTE: +The best practice is 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. It also avoids situations, +such as short pages, which can expose the presence of confidential resources. + +See [`authorization_spec.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/graphql/features/authorization_spec.rb) +for examples of all the authorization schemes discussed here. + +## Type authorization + +Authorize a type by passing an ability to the `authorize` method. All +fields with the same type is 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 +``` + +## Resolver authorization + +Resolvers can have their own authorizations, which can be applied either to the +parent object or to the resolved values. + +An example of a resolver that authorizes against the parent is +`Resolvers::BoardListsResolver`, which requires that the parent +satisfy `:read_list` before it runs. + +An example which authorizes against the resolved resource is +`Resolvers::Ci::ConfigResolver`, which requires that the resolved value satisfy +`:read_pipeline`. + +To authorize against the parent, the resolver must _opt in_ (because this +was not the default value initially), by declaring this with `authorizes_object!`: + +```ruby +module Resolvers + class MyResolver < BaseResolver + authorizes_object! + + authorize :some_permission + end +end +``` + +To authorize against the resolved value, the resolver must apply the +authorization at some point, typically by using `#authorized_find!(**args)`: + +```ruby +module Resolvers + class MyResolver < BaseResolver + authorize :some_permission + + def resolve(**args) + authorized_find!(**args) # calls find_object + end + + def find_object(id:) + MyThing.find(id) + end + end +end +``` + +Of the two approaches, authorizing the object is more efficient, because it +helps avoid unnecessary queries. + +## Field authorization + +Fields can be authorized with the `authorize` option. + +Fields authorization is checked against the current object, and +authorization happens _before_ resolution, which means that +fields do not have access to the resolved resource. If you need to +apply an authorization check to a field, you probably want to add +authorization to the resolver, or ideally to the type. + +For example, the following authorization ensures that the currently +authenticated user must have administrator level access to the project +to view the `secretName` field: + +```ruby +module Types + class ProjectType < BaseObject + field :secret_name, ::GraphQL::STRING_TYPE, null: true, authorize: :owner_access + end +end +``` + +In this example, we use field authorization to make an efficient check (basically +`Ability.allowed?(current_user, :read_transactions, bank_account)`) +and thereby save making a more expensive query: + +```ruby +module Types + class BankAccountType < BaseObject + field :transactions, ::Types::TransactionType.connection_type, null: true, + authorize: :read_transactions + end +end +``` + +Field authorization is a good idea for: + +- Scalar fields (strings, booleans, or numbers) that should have different levels + of access controls to other fields. +- Object and collection fields where an access check can be applied to the + parent to save the field resolution, and avoid individual policy checks + on each resolved object. + +Field authorization is not a replacement for object level checks, unless the +objects' access level can be determined completely by the access level of the +parent. For instance, we should not use this for `Project.issue`, since +each issue can be confidential, separately of the access level of the parent +project. + +You can also authorize fields against multiple abilities. Pass the abilities +as an array instead of as a single value: + +```ruby +module Types + class MyType < BaseObject + field :hidden_field, ::GraphQL::INT_TYPE, + null: true, + authorize: [:owner_access, :another_ability] + end +end +``` + +This implies the following tests: + +```ruby +Ability.allowed?(current_user, :owner_access, object_of_my_type) && + Ability.allowed?(current_user, :another_ability, object_of_my_type) +``` + +## 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` on the user and `second_permission` on the +issue 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 +``` + +This implies the following tests: + +```ruby +Ability.allowed?(current_user, :second_permission, issue) && + Ability.allowed?(current_user, :first_permission, issue.author) +``` diff --git a/doc/development/graphql_guide/index.md b/doc/development/graphql_guide/index.md index 4ecb34835aaec9..b8d4b53992e9a1 100644 --- a/doc/development/graphql_guide/index.md +++ b/doc/development/graphql_guide/index.md @@ -17,6 +17,7 @@ feedback, and suggestions. - [GraphQL API documentation style guide](../documentation/graphql_styleguide.md): documentation style guide for GraphQL. - [GraphQL API](../../api/graphql/index.md): user documentation for the GitLab GraphQL API. +- [GraphQL authorization](authorization.md): guide to using authorization in GraphQL. - [GraphQL BatchLoader](batchloader.md): development documentation on the BatchLoader. - [GraphQL pagination](pagination.md): development documentation on pagination. - [GraphQL Pro](graphql_pro.md): information on our GraphQL Pro subscription. -- GitLab From 4194a6de9f1e980dd7303319bad8a58c01db99a9 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Tue, 6 Apr 2021 22:28:59 +0000 Subject: [PATCH 2/2] Apply 8 suggestion(s) to 1 file(s) --- .../graphql_guide/authorization.md | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/doc/development/graphql_guide/authorization.md b/doc/development/graphql_guide/authorization.md index 62cc284264b69d..ee5713f6fdaa55 100644 --- a/doc/development/graphql_guide/authorization.md +++ b/doc/development/graphql_guide/authorization.md @@ -135,7 +135,7 @@ fields do not have access to the resolved resource. If you need to apply an authorization check to a field, you probably want to add authorization to the resolver, or ideally to the type. -For example, the following authorization ensures that the currently +For example, the following authorization ensures that the authenticated user must have administrator level access to the project to view the `secretName` field: @@ -147,9 +147,9 @@ module Types end ``` -In this example, we use field authorization to make an efficient check (basically -`Ability.allowed?(current_user, :read_transactions, bank_account)`) -and thereby save making a more expensive query: +In this example, we use field authorization (such as +`Ability.allowed?(current_user, :read_transactions, bank_account)`) to avoid +a more expensive query: ```ruby module Types @@ -160,7 +160,7 @@ module Types end ``` -Field authorization is a good idea for: +Field authorization is recommended for: - Scalar fields (strings, booleans, or numbers) that should have different levels of access controls to other fields. @@ -168,11 +168,10 @@ Field authorization is a good idea for: parent to save the field resolution, and avoid individual policy checks on each resolved object. -Field authorization is not a replacement for object level checks, unless the -objects' access level can be determined completely by the access level of the -parent. For instance, we should not use this for `Project.issue`, since -each issue can be confidential, separately of the access level of the parent -project. +Field authorization does not replace object level checks, unless the object +precisely matches the access level of the parent project. For example, issues +can be confidential, independent of the access level of the parent. Therefore, +we should not use field authorization for `Project.issue`. You can also authorize fields against multiple abilities. Pass the abilities as an array instead of as a single value: @@ -187,7 +186,7 @@ module Types end ``` -This implies the following tests: +The field authorization on `MyType.hiddenField` implies the following tests: ```ruby Ability.allowed?(current_user, :owner_access, object_of_my_type) && @@ -196,13 +195,13 @@ Ability.allowed?(current_user, :owner_access, object_of_my_type) && ## 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. +Authorizations are cumulative. In other words, the currently authenticated +user may need to pass authorization requirements on both a field and a field's +type. In the following simplified example the currently authenticated user -would need both `first_permission` on the user and `second_permission` on the -issue in order to see the author of the issue. +needs both `first_permission` on the user and `second_permission` on the +issue to see the author of the issue. ```ruby class UserType @@ -216,7 +215,7 @@ class IssueType end ``` -This implies the following tests: +The combination of the object authorization on `UserType` and the field authorization on `IssueType.author` implies the following tests: ```ruby Ability.allowed?(current_user, :second_permission, issue) && -- GitLab