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.rbapp/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::Builderinapp/models/authz/granular_scopes/builder.rb.- It takes normalised inputs and a
boundary_rule:object. The rule responds toallowed_resource?(resource)andpersonal_projects_namespace. - It returns unsaved records in input order.
- It raises
Builder::ResourceNotAllowedErroron 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/authzbecause it changes no state, likeAuthz::Boundary.
- It takes normalised inputs and a
- New
Authz::GranularScopes::ReadBoundaryRuleis the user-owned rule. It usesAbility.allowed?(user, :read_boundary, resource)anduser.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 checkread_boundaryagainst the impersonated user. The helper loads groups and projects by ID, normalises, and calls the builder. It mapsResourceNotAllowedErrortonot_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
- Work item: #631824 (closed)
- Parent stream: #630327
- Follow-up that consumes both MRs: #631825 (closed)
- Companion MR, the uniqueness check. The two MRs are independent and both target
master: !259531 (merged)
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.
- Create a token with one
selected_membershipsentry 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. - Create a token with an entry naming a private project the user cannot read. Expect HTTP 404.
- Create a token with a
personal_projectsentry. Expect HTTP 201 and a scope whose namespace equals the user's namespace. - Run the same creation through GraphiQL at
/-/graphql-explorer. Use thepersonalAccessTokenCreatemutation withresourceIdsfor 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.