Include granular_scopes in service account personal access token list API responses

Summary

The two routes that list a service account's personal access tokens present each token with API::Entities::PersonalAccessToken and no options:

  • GET /groups/:id/service_accounts/:user_id/personal_access_tokens
  • GET /projects/:id/service_accounts/:user_id/personal_access_tokens

That entity exposes granular_scopes only when the token is granular and options[:with_granular_scopes] is set. So a fine-grained token is listed with granular: true and scopes: ["granular"], and nothing says what the token can access.

GET /personal_access_tokens already passes the options from granular_scopes_options_for, and since !260929 (merged) (#632896 (closed)) so do the rotate routes of these same two API classes. I noted the list routes in !260929 (merged) and left them out, because #632896 (closed) covers rotation only. This issue is for the list routes.

Steps to reproduce

A service account can hold a fine-grained token today through the generic routes that create one for the caller, as discussed in #573355. For example:

  1. As an Owner of a group, create a group service account and a personal access token for it with the api scope (POST /groups/:id/service_accounts/:user_id/personal_access_tokens).

  2. Authenticated with that token, create a fine-grained token for the service account itself:

    curl --request POST \
      --header "PRIVATE-TOKEN: <service_account_token>" \
      --header "Content-Type: application/json" \
      --data '{"name": "fine-grained", "granular_scopes": [{"access": "all_memberships", "permissions": ["read_job"]}]}' \
      --url "https://gitlab.example.com/api/v4/user/personal_access_tokens"

    The response of this route includes granular_scopes.

  3. As the group Owner, list the service account's tokens with GET /groups/:id/service_accounts/:user_id/personal_access_tokens.

  4. Do the same for a project service account with GET /projects/:id/service_accounts/:user_id/personal_access_tokens.

I found this by reading the code on master and have not run these steps against an instance. The presentation itself is checked under "Relevant logs" below.

Example Project

Not applicable: any service account that holds a fine-grained token shows it.

What is the current bug behavior?

Each fine-grained token in the list has granular: true and scopes: ["granular"], but no granular_scopes. The same token is presented with its granular_scopes by GET /personal_access_tokens and by the rotate routes of these two classes.

What is the expected correct behavior?

Each fine-grained token in the list has granular_scopes, with project_id and group_id resolved as on the other routes. Tokens that are not fine-grained are presented as they are today, and the routes run no extra query for them.

Relevant logs and/or screenshots

I presented a stand-in fine-grained token through API::Entities::PersonalAccessToken as it is on master, with grape-entity 1.1.0 (the version Gemfile.lock pins) on Ruby 3.3.11. I did it once with no options, as the list routes do, and once with the options granular_scopes_options_for returns:

no options:           {"id":1,"name":"sa-granular","revoked":false,"created_at":null,"description":null,"scopes":["granular"],"user_id":7,"last_used_at":null,"last_used_ips":[],"active":true,"granular":true,"expires_at":null}
with_granular_scopes: {"id":1,"name":"sa-granular","revoked":false,"created_at":null,"description":null,"scopes":["granular"],"user_id":7,"last_used_at":null,"last_used_ips":[],"granular_scopes":[{"access":"selected_memberships","permissions":["read_job"],"project_id":null,"group_id":42}],"active":true,"granular":true,"expires_at":null}

Output of checks

Found on master at f967fc7d. I have not reproduced it on GitLab.com.

Possible fixes

Both list routes end with present paginate_with_strategies(service_account_pats), with: Entities::PersonalAccessToken and pass no options:

The entity exposes granular_scopes only with options[:with_granular_scopes] (lib/api/entities/personal_access_token.rb:25-27).

The fix is to present the page with **granular_scopes_options_for(tokens), as GET /personal_access_tokens does (lib/api/personal_access_tokens.rb:42-45) and the rotate routes of the same classes do (lib/api/group_service_accounts.rb:341-343, lib/api/project_service_accounts.rb:315-317):

tokens = paginate_with_strategies(service_account_pats)

present tokens, with: Entities::PersonalAccessToken, **granular_scopes_options_for(tokens)

Both classes already include API::Helpers::PersonalAccessTokensHelpers. The helper (lib/api/helpers/personal_access_tokens_helpers.rb:117-130) preloads the scopes and namespaces of the granular tokens on the page. For a page with no granular token it returns no options and runs no query.

The request specs of both routes would cover a granular token listed with its granular_scopes (with project_id for a project scope), a legacy token listed without the key and with no query on granular_scopes, and a query count that does not grow with the number of granular tokens. The two list sections of doc/api/service_accounts.md would get a history item and the same note as the rotate sections.

I'd like to work on this.

Edited by 🤖 GitLab Bot 🤖