Share granular scope builder for personal access tokens

What does this MR do and why?

Part of #631824 (closed).

This MR moves the granular scope builder into one shared class. Behaviour does not change.

Two copies of the builder existed:

  • lib/api/helpers/personal_access_tokens_helpers.rb
  • app/graphql/mutations/users/personal_access_tokens/create.rb

Both turned {access, permissions, resources} inputs into unsaved Authz::GranularScope records. Both derived the namespace from the access level. Both guarded each group or project with Ability.allowed?(user, :read_boundary, resource).

Authz::GranularScope has no validation tying access level to namespace. The builders are the only thing keeping them consistent. OAuth applications, in the follow-up, need the same derivation with a different boundary rule per application owner. They would have become a third copy.

What changes

  • New Authz::GranularScopes::Builder in app/models/authz/granular_scopes/builder.rb.
    • It takes normalised inputs and a boundary_rule: object. The rule responds to allowed_resource?(resource) and personal_projects_namespace.
    • It returns unsaved records in input order.
    • It raises Builder::ResourceNotAllowedError on the first refused resource.
    • It does not load records by ID, set organization_id, deduplicate or persist.
    • It is a plain object under app/models/authz because it changes no state, like Authz::Boundary.
  • New Authz::GranularScopes::ReadBoundaryRule is the user-owned rule. It uses Ability.allowed?(user, :read_boundary, resource) and user.namespace. It is an object rather than a lambda because two behaviours are needed. The follow-up adds rules for group-owned and instance applications, which have no user.
  • The REST helper keeps build_granular_scopes(current_user, inputs). Impersonation tokens still check read_boundary against the impersonated user. The helper loads groups and projects by ID, normalises, and calls the builder. It maps ResourceNotAllowedError to not_found! as before.
  • The GraphQL mutation batch-loads from global IDs, syncs, and calls the builder. It maps the error to raise_resource_not_available_error! as before.

References

How to set up and validate locally

Use a GDK instance. <token> is a personal access token with the api scope. The granular_personal_access_tokens flag is on by default.

  1. Create a token with one selected_memberships entry naming one group and one project the user is a member of. Expect HTTP 201 and two scopes. Their namespaces are the group and the project's namespace.
  2. Create a token with an entry naming a private project the user cannot read. Expect HTTP 404.
  3. Create a token with a personal_projects entry. Expect HTTP 201 and a scope whose namespace equals the user's namespace.
  4. Run the same creation through GraphiQL at /-/graphql-explorer. Use the personalAccessTokenCreate mutation with resourceIds for the same group and project. Expect the same two scopes as in step 1.

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.

Edited by Eugie Limpin

Merge request reports

Loading
Loading