Check parent_authorizes permissions at every field
What does this MR do and why?
A GraphQL type with authorize_granular_token skip_reason: :parent_authorizes has no fine-grained token check of its own. It relies on whatever permission its parent checked, and that permission differs from parent to parent. For example, a token needs read_issue to reach WorkItemType through Issue.workItemType, and read_work_item through WorkItem.workItemType. A new field could expose a skipped type with an unrelated permission, and gitlab:permissions:validate still passed.
This MR implements step 2 of the plan in #637600:
authorize_granular_token parent_authorizes: :permission_namedeclares the permission a skipped type needs. The type still skips its own check.- A field without its own directive that returns a declared type checks that permission on the boundary of its parent object, before the resolver runs.
BaseFieldfinds the boundary through the directives of the concrete parent type, andGranularScopeAuthorizationgets apermissions:override for this. ParentAuthorizesCheckruns ingitlab:permissions:validateand checks the fields where the runtime check does not apply:- A field with its own directive must include the declared permission.
- A root field that returns the type must have its own directive, and a mutation that returns it must have a real token check.
- Every path to a field on another skipped type must check the declared permission, or pass through a mutation with its own token check. This reuses the path walker from the first version of this MR.
- The declared permission must belong to an assignable permission for the parent's boundary types, and must be a project or group policy ability.
- A non-null field must not return a type whose permission is outside the parent's assignable permissions, because a denied check would null the parent.
- A field without its own directive that returns an interface or union needs all possible types to declare the same permission. Otherwise the runtime check does not run, so the field needs its own directive.
- Types that a token-checked type returns, but that have not declared a permission, are listed as
parent_authorizes:entries inconfig/authz/graphql/authorization_todo.txt. There are 132 of them.
No type declares a permission yet, so runtime behavior does not change. Owning teams declare their types in follow-up MRs.
The first version of this MR added a parent_permissions: list that only validation checked. It compared permission names and changed nothing at runtime, so this version replaces it.
Only requests with a granular token pay for the new check. Results are cached per request by permission and boundary. A spec in spec/graphql/types/base_field_spec.rb runs a query with a granular token and checks that a list of two items in one project calls AuthorizeGranularScopesService once per permission.
References
- #637600, the plan
- https://gitlab.com/gitlab-org/gitlab/-/work_items/636258, the problem
- !266080 (comment 4128434393), where this came up
Screenshots or screen recordings
Not applicable, no UI change.
How to set up and validate locally
-
Run
bundle exec rake gitlab:permissions:validate. It passes. -
Remove
parent_authorizes:Achievementfromconfig/authz/graphql/authorization_todo.txtand run the task again. It reportsAchievementas a type that declares no permission. -
Run the specs for the field check and the validation:
bundle exec rspec spec/graphql/types/base_field_spec.rb spec/tasks/gitlab/permissions/graphql/parent_authorizes_check_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.