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_name declares 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. BaseField finds the boundary through the directives of the concrete parent type, and GranularScopeAuthorization gets a permissions: override for this.
  • ParentAuthorizesCheck runs in gitlab:permissions:validate and 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 in config/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

Screenshots or screen recordings

Not applicable, no UI change.

How to set up and validate locally

  1. Run bundle exec rake gitlab:permissions:validate. It passes.

  2. Remove parent_authorizes:Achievement from config/authz/graphql/authorization_todo.txt and run the task again. It reports Achievement as a type that declares no permission.

  3. 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.

🤖 Generated with Claude Code

Edited by Alex Buijs

Merge request reports

Loading
Loading