Skip to content

Create a Group API endpoint for self-revoking a token

Nick Malcolm requested to merge 460779-token-revocation into master

What does this MR do and why?

See Token Revocation Endpoint scoped to Groups (#460777 - closed)

  • Creates a "token agnostic" service that can route revocation requests to the correct Revoker IF AND ONLY IF the token can affect the group
  • Creates an API endpoint, scoped to groups, that will call AgnosticTokenRevocationService
  • Supports PersonalAccessToken (all types) and DeployToken (group only) (see: Identify an MVP set of revocable tokens (#460778 - closed))
  • Feature flagged by top-level group to control the rollout
  • Log / Audit Event for this API endpoint
    • API endpoints are already logged: can search for path and get IP address
    • PATs have their own Audit event added by the EE extension
    • DeployTokens has one; needs updating to include source of revocation
  • TBC refactor so that, instead of DELETE with PRIVATE-TOKEN=x we instead follow the pattern of https://docs.gitlab.com/ee/development/sec/token_revocation_api.html#post-v1revoke_tokens
  • Rate limiting
  • Documentation
  • Refactor to be an internal API using a shared secret
  • Refactor to be a regular API requiring Group Owner permission

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Screenshots or screen recordings

Screenshots are required for UI changes, and strongly recommended for all other merge requests.

Before After

How to set up and validate locally

Create a Group, and any of:

  • A Personal Access Token for a guest+ member of that group
  • A Group Access Token for that group or a subgroup
  • A Project Access Token for a project in that group or a subgroup
  • A Group Deploy Token for that group or a subgroup
Feature.enable(:group_agnostic_token_revocation, Group.find(ID))

As a group owner, create an api scoped PAT.

Passing a token that doesn't belong to that group:

% curl -XPOST --header "PRIVATE-TOKEN: OWNER_PAT" https://gdk.test:3443/api/v4/groups/ID/tokens/revoke -H "Content-Type: application/json" --data '{"token":"ANOTHER_TOKEN"}'
{"message":"Not found"}

Or:

% curl --header "PRIVATE-TOKEN: OWNER_PAT" -XPOST https://gdk.test:3443/api/v4/groups/ANOTHER_ID/tokens/revoke -H "Content-Type: application/json" --data '{"token":"ACTUALTOKEN"}'
{"message":"Unauthorized"}

If we pass a deploy token that belongs to that group:

% curl -XPOST --header "PRIVATE-TOKEN: OWNER_PAT" https://gdk.test:3443/api/v4/groups/ID/tokens/revoke -H "Content-Type: application/json" --data '{"token":"ACTUALTOKEN"}'
{
    "id": 9,
    "name": "tokensubgroupa-deploytoken",
    "username": "gitlab+deploy-token-9",
    "expires_at": null,
    "scopes":
    [
        "read_repository",
        "read_package_registry",
        "write_package_registry"
    ],
    "revoked": true,
    "expired": false
}

If you have a group with Audit Events configured:

AuditEvent Create (3.0ms)  INSERT INTO "audit_events" ("author_id", "entity_id", "entity_type", "details", "ip_address", "author_name", "entity_path", "target_details", "created_at", "target_type", "target_id") VALUES (22, 116, 'Group', '---
:action: :custom
:revocation_source: :group_token_revocation_service
:author_name: Nick Malcolm
:author_class: User
:target_id: 9
:target_type: DeployToken
:target_details: tokensubgroupa-deploytoken
:custom_message: ''Revoked group deploy token with name: tokensubgroupa-deploytoken
  with token_id: 9 with scopes: [:read_repository, :read_package_registry, :write_package_registry].''
:ip_address: 172.16.123.123
:entity_path: tokengroup/tokensubgroupa
', '172.16.123.123/32', 'Nick Malcolm', 'tokengroup/tokensubgroupa', 'tokensubgroupa-deploytoken', '2024-05-27 01:46:55.303366', 'DeployToken', 9) RETURNING "id" /*application:web,correlation_id:01HYVVNAT692RQXYMDC8RQKHR2,endpoint_id:POST /api/:version/groups/:id/tokens/revoke,db_config_name:main,line:/lib/gitlab/audit/auditor.rb:181:in `log_to_database'*/

All requests are also logged in the API logs (abridged):

==> log/api_json.log <==
{
  "time":"2024-05-27T01:46:55.410Z",
  "severity":"INFO",
  "status":201,
  "method":"POST",
  "path":"/api/v4/groups/63/tokens/revoke",
  "params":[{"key":"token","value":"[FILTERED]"}],
  "host":"gdk.test",
  "remote_ip":"172.16.123.123, 172.16.123.123",
  "route":"/api/:version/groups/:id/tokens/revoke",
  "rate_limiting_gates":["group_token_revocation_unauthenticated_by_ip","group_token_revocation_unauthenticated_by_token"],
  "correlation_id":"01HYVVNAT692RQXYMDC8RQKHR2",
  "meta.caller_id":"POST /api/:version/groups/:id/tokens/revoke",
  "meta.feature_category":"system_access",
  "request_urgency":"low",
}

Related to #460779 (closed)

Edited by Nick Malcolm

Merge request reports