Check the credentials inventory license in the group API

On hold: waits for the product decision on #632436.

What does this MR do and why?

The group credentials inventory API (ee/lib/api/manage/groups.rb) checks only that GitLab.com serves the feature and that the caller has admin_group on the group. The credentials inventory page and its sidebar entry check read_group_credentials_inventory instead, which EE::GroupPolicy prevents unless the group is licensed for credentials_inventory and has no parent. So, as the issue describes, the Owner of a top-level group on a plan without the feature can list, revoke and rotate the personal and resource access tokens, and list and delete the SSH keys, that the page refuses to show. The list routes also answer for a subgroup.

This MR follows the proposal in the issue. The before block now authorizes read_group_credentials_inventory, and the revoke, rotate and delete routes also authorize admin_group_credentials_inventory, the permission the policy pairs with it. Both are granted to the same roles as admin_group: the Owner (config/authz/roles/owner.yml) and an administrator in admin mode (admin.yml inherits the Owner role). So the only requests that lose access are those for a group without the license or for a subgroup, whoever makes them, and they get 403 Forbidden. The SaaS check, and its 404 on self-managed, are unchanged.

Caller Before After
Owner of a top-level group licensed for the credentials inventory Served Served (unchanged)
Owner of a top-level group on Free or Premium Passes the permission check of every route 403 on every route
Owner calling a route with the ID of a subgroup List routes 200 with the subgroup's own data 403 on every route
Maintainer of the group 403 403 (unchanged)
Administrator in admin mode, for a licensed top-level group Served Served (unchanged)
Administrator in admin mode, for any other group Passes the permission check of every route 403 on every route, as for the Owner
Any caller on self-managed 404 404 (unchanged)

This changes behavior on GitLab.com. The Owners of top-level groups whose plan does not include the credentials inventory, which means Free and Premium, get past the permission check of these eight routes today. After this MR, they get 403 Forbidden, and so does any call with the ID of a subgroup. An administrator in admin mode gets the same 403 for those groups. Nothing changes for Ultimate top-level groups or on self-managed instances.

The team has not decided yet whether to make this change, or how. @vwolanyk handed the issue to groupauthentication for a decision about the license check, because it takes these routes away from groups that may be using them. On 2026-10-13 I asked @vwolanyk and @NellyVahab on the issue whether to ship the check as proposed, behind a feature flag, or after announcing it (my question). There is no answer yet, so I am opening the MR as proposed, to give the team a concrete change to decide on. It follows the tier that the feature documentation, GitlabSubscriptions::Features::ULTIMATE_FEATURES and the routes' own route_setting :tier, :ultimate already state. The REST API style guide counts a change to what an endpoint requires for authorization as a breaking change, so the team may prefer to take this through the breaking change exception process rather than ship it as a bug fix. If the team would rather put the check behind a feature flag, announce it first, or handle it another way, I am happy to rework the MR.

The request specs showed how this went unnoticed: they ran under the default test license, which has no Ultimate features, so every route was tested on a plan without credentials_inventory. They now stub the license once, in a licensed_features helper that a describe block extends (two of them add domain_verification). A second stub_licensed_features call resets what an earlier call stubbed, so a separate stub in those blocks would have undone the license. ee/spec/requests/api/api_spec.rb calls the personal access token list route for its DPoP examples, so it gets the license too.

The commits:

  1. Check the credentials inventory license in the group API: the authorization change, the spec setup above, and request specs for a group without the license (all eight routes) and for a subgroup (the three list routes). It carries the Changelog: fixed and EE: true trailers and describes the whole change, so it is the right message if the MR is squashed.
  2. Cover who may list the group credentials inventory: the list routes were covered for a non-member only. This adds a Maintainer (refused) and an administrator with and without admin mode (served, refused). These examples pass with the API file from master and with this MR, which shows the move away from admin_group kept the same roles.
  3. Document the Ultimate tier of the group credentials API: the credentials inventory section of doc/api/groups.md said Premium and Ultimate. It now says Ultimate, records the move in its history for GitLab 19.6, and asks for the Owner role on the top-level group, since a subgroup is refused.

I left one related place alone. ee/app/policies/ee/personal_access_token_policy.rb also checks only Gitlab::Saas.feature_available?(:group_credentials_inventory) before it lets an enterprise user's manager revoke and rotate their tokens. Other endpoints use those abilities too, so changing them belongs in the team's decision rather than in this MR.

!263025 (merged), which changed three of the same files to return granular_scopes from the personal access token list route, was merged on 2026-10-19. The two changes touch different lines: git merge-tree combined this branch with it without a conflict, and both request spec files passed on the result (190 examples, 0 failures).

References

Closes #632436

  • !215796 (merged) made the API generally available and removed its manage_pat_by_group_owners_ready flag check, which left the SaaS check and admin_group.
  • !260136 (merged) added route_setting :tier, :ultimate to these routes.

Screenshots or screen recordings

Not a UI change. GET /api/v4/groups/:id/manage/personal_access_tokens as the group Owner on GitLab.com, from the request specs:

Group Before After
Top-level group without the credentials_inventory license 200 with the tokens 403 Forbidden
Subgroup 200 [] 403 Forbidden
Specs I ran

All on master at 01c4de0ecc6, in EE, with bundle exec rspec ee/spec/requests/api/manage/groups_spec.rb ee/spec/requests/api/api_spec.rb:

  • Commit 1 with ee/lib/api/manage/groups.rb from master: 178 examples, 11 failures, exactly the new examples. The eight for a group without the license fail with expected the response to have status code :forbidden but it was 200 (the list and rotate routes) or 204 (revoke and delete), and the three for a subgroup with but it was 200. The response was: [].
  • Commit 1: 178 examples, 0 failures.
  • Commit 2 with ee/lib/api/manage/groups.rb from master: 187 examples, 11 failures, the same 11. The nine examples commit 2 adds pass.
  • Commit 2: 187 examples, 0 failures.
  • The API change with both spec files from master, which run under the default test license: 90 of 167 examples failed, all of them with 403 Forbidden where they expected another status, a success in most of them. That is the run that shows the existing specs had never had the feature licensed.
  • RuboCop on the Ruby files of each commit: no offenses. Both commit message linters (scripts/lint/commit_linter.rb and the Danger one) on each commit: no problems. git diff --check: clean.
  • doc/api/groups.md: markdownlint and scripts/lint-doc.sh pass, and Vale reports the same findings as on master, none of them in this section.

How to set up and validate locally

  1. In a GDK that simulates GitLab.com, with namespace plans checked, create a top-level group on the Premium plan, add an enterprise user, and create a personal access token for the group Owner.
  2. Call curl --header "PRIVATE-TOKEN: <owner token>" "http://gdk.test:3000/api/v4/groups/<group ID>/manage/personal_access_tokens".
  3. Before this MR the response is 200 with the tokens; with it, 403 Forbidden. The same holds for the resource access token and SSH key routes and for the revoke, rotate and delete routes.
  4. Move the group to the Ultimate plan: the routes answer as before.
  5. Call any list route with the ID of a subgroup: 403 Forbidden.

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 José M. Requena Plens

Merge request reports

Loading
Loading