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_tokensGET /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:
-
As an Owner of a group, create a group service account and a personal access token for it with the
apiscope (POST /groups/:id/service_accounts/:user_id/personal_access_tokens). -
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. -
As the group Owner, list the service account's tokens with
GET /groups/:id/service_accounts/:user_id/personal_access_tokens. -
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.