Add revoke endpoint for runner controller token API
What does this MR do and why?
Add revoke endpoint for runner controller token API.
This change adds the ability to revoke (disable) runner controller tokens. Tokens can be marked as "revoked" to prevent their use while keeping them in the system for audit purposes.
The implementation adds a status field to tokens that can be either "active" or "revoked", with active being the default. When listing or viewing tokens, only active ones are shown. A new API endpoint allows admins to revoke tokens.
The database is updated to store the token status, and comprehensive tests ensure the feature works correctly.
References
Part of #582812.
How to set up and validate locally
- Set up GDK and prepare instance admin's personal access token.
- Verify the endpoints using
curl.
export PAT="your-personal-access-token"
Prerequisites - create runner controller & token
Create runner controller if not exists:
curl --request POST \
--header "Content-Type: application/json" \
--header "PRIVATE-TOKEN: $PAT" \
"https://gdk.test:3443/api/v4/runner_controllers"
Create new token:
curl --request POST \
--header "Content-Type: application/json" \
--header "PRIVATE-TOKEN: $PAT" \
--data '{
"description": "Validates runner security settings before registration"
}' \
"https://gdk.test:3443/api/v4/runner_controllers/1/tokens"
Get tokens
curl --request GET \
--header "Content-Type: application/json" \
--header "PRIVATE-TOKEN: $PAT" \
"https://gdk.test:3443/api/v4/runner_controllers/1/tokens"
Revoke a token:
curl --request DELETE \
--header "Content-Type: application/json" \
--header "PRIVATE-TOKEN: $PAT" \
"https://gdk.test:3443/api/v4/runner_controllers/1/tokens/1"
Database query plans
List runner controller tokens
This MR adds a filter by status.
SELECT "ci_runner_controller_tokens".*
FROM "ci_runner_controller_tokens"
WHERE "ci_runner_controller_tokens"."runner_controller_id" = 1
AND "ci_runner_controller_tokens"."status" = 0
ORDER BY "ci_runner_controller_tokens"."id" ASC limit 20 offset 0
Limit (cost=0.12..3.15 rows=1 width=98) (actual time=0.014..0.046 rows=1 loops=1)
Buffers: shared hit=3 dirtied=1
WAL: records=1 fpi=1 bytes=129
-> Index Scan using ci_runner_controller_tokens_pkey on public.ci_runner_controller_tokens (cost=0.12..3.15 rows=1 width=98) (actual time=0.013..0.044 rows=1 loops=1)
Filter: ((ci_runner_controller_tokens.runner_controller_id = 1) AND (ci_runner_controller_tokens.status = 0))
Rows Removed by Filter: 0
Buffers: shared hit=3 dirtied=1
WAL: records=1 fpi=1 bytes=129
Settings: work_mem = '100MB', effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4'
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141638
Without composite index
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141632
Limit (cost=3.17..3.17 rows=1 width=98) (actual time=0.067..0.068 rows=1 loops=1)
Buffers: shared hit=8
-> Sort (cost=3.17..3.17 rows=1 width=98) (actual time=0.066..0.066 rows=1 loops=1)
Sort Key: ci_runner_controller_tokens.id
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=8
-> Index Scan using index_ci_runner_controller_tokens_on_status on public.ci_runner_controller_tokens (cost=0.14..3.16 rows=1 width=98) (actual time=0.045..0.046 rows=1 loops=1)
Index Cond: (ci_runner_controller_tokens.status = 0)
Filter: (ci_runner_controller_tokens.runner_controller_id = 1)
Rows Removed by Filter: 0
Buffers: shared hit=5
Settings: jit = 'off', seq_page_cost = '4', work_mem = '100MB', effective_cache_size = '338688MB', random_page_cost = '1.5'
Get a single token
This MR adds a filter by status.
SELECT "ci_runner_controller_tokens".*
FROM "ci_runner_controller_tokens"
WHERE "ci_runner_controller_tokens"."runner_controller_id" = 1
AND "ci_runner_controller_tokens"."status" = 0
AND "ci_runner_controller_tokens"."id" = 13 limit 1
Limit (cost=0.12..3.15 rows=1 width=98) (actual time=0.047..0.048 rows=0 loops=1)
Buffers: shared hit=8
-> Index Scan using index_ci_runner_controller_tokens_on_rc_id_and_status on public.ci_runner_controller_tokens (cost=0.12..3.15 rows=1 width=98) (actual time=0.046..0.046 rows=0 loops=1)
Index Cond: ((ci_runner_controller_tokens.runner_controller_id = 1) AND (ci_runner_controller_tokens.status = 0))
Filter: (ci_runner_controller_tokens.id = 13)
Rows Removed by Filter: 1
Buffers: shared hit=8
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4', work_mem = '100MB'
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141639
Without composite index
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141633
Limit (cost=0.14..3.16 rows=1 width=98) (actual time=0.054..0.055 rows=1 loops=1)
Buffers: shared hit=5
-> Index Scan using index_ci_runner_controller_tokens_on_status on public.ci_runner_controller_tokens (cost=0.14..3.16 rows=1 width=98) (actual time=0.053..0.053 rows=1 loops=1)
Index Cond: (ci_runner_controller_tokens.status = 0)
Filter: ((ci_runner_controller_tokens.runner_controller_id = 1) AND (ci_runner_controller_tokens.id = 1))
Rows Removed by Filter: 0
Buffers: shared hit=5
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4', work_mem = '100MB'
Revoke a token
This is a new query introduced by this MR when revoking it.
UPDATE "ci_runner_controller_tokens"
SET "updated_at" = '2025-12-11 05:48:55.805493',
"status" = 1
WHERE "ci_runner_controller_tokens"."id" = 3
ModifyTable on public.ci_runner_controller_tokens (cost=0.12..3.14 rows=0 width=0) (actual time=0.205..0.205 rows=0 loops=1)
Buffers: shared hit=28 dirtied=3
WAL: records=6 fpi=2 bytes=608
-> Index Scan using ci_runner_controller_tokens_pkey on public.ci_runner_controller_tokens (cost=0.12..3.14 rows=1 width=16) (actual time=0.023..0.023 rows=1 loops=1)
Index Cond: (ci_runner_controller_tokens.id = 1)
Buffers: shared hit=5
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4', work_mem = '100MB'
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141640
Without composite index
https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/46428/commands/141634
ModifyTable on public.ci_runner_controller_tokens (cost=0.14..3.16 rows=0 width=0) (actual time=0.252..0.253 rows=0 loops=1)
Buffers: shared hit=27
WAL: records=5 fpi=0 bytes=350
-> Index Scan using ci_runner_controller_tokens_pkey on public.ci_runner_controller_tokens (cost=0.14..3.16 rows=1 width=16) (actual time=0.026..0.027 rows=1 loops=1)
Index Cond: (ci_runner_controller_tokens.id = 1)
Buffers: shared hit=5
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', jit = 'off', seq_page_cost = '4', work_mem = '100MB'
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.