Admin toggle individual group runner registration
What does this MR do and why?
This MR expands on admin runner registration management feature flag runner_registration_control by allowing admins to restrict group runner registration on a group by group basis. In prior, it was only specifiable on an ‘all group’ basis under the admin settings, CI/CD page.
The new setting ‘runner_registration_enabled’ is held as a boolean column in the namespace_settings table. By default, it is enabled.
When ‘all group’ runner registration is disabled, all groups have disabled runner registration regardless of group by group specifications set.
When ‘all group’ runner registration is enabled, runner registration is permitted based on the group level ‘runner_registration_enabled’.
- The second commit changes this behavior: Runner registration is permitted based on all parent groups being permitted. Disabling a parent group disables runner registration for all subgroups.
As before, the admin account will always be able to register group runners.
When runner_registration_enabled
is modified, the group's runner registration token is reset to prevent users from using a token they may have seen prior to the admin setting change.
Screenshots or screen recordings
The admin groups page is where the setting can be modified.
Disabling group runners causes the blue "Register a group runner" dropdown to disappear. becomes,
How to set up and validate locally
1. Enable runner registration control on the GDK console
```ruby
Feature.enable(:runner_registration_control)
```
-
Visit the admin settings, CI/CD page and verify that under Runner Registration, “Members of the group can register runners” is checked.
-
Visit the admin page and navigate to the group list
-
Select edit for <group_name> so that you are on the following form
http://gdk.test:3000/admin/groups/<group_name>/edit
-
Under the "Permissions and group features" section, modify the “Runner registration” checkbox and save changes.
-
Log in as a member of the modified group
-
Navigate to the <group_name> CI/CD page and verify that the dropdown “Register a group runner” is visible/invisible as specified.
http://gdk.test:3000/groups/<group_name>/-/runners
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.
Database query plans
Postgres.ai link: https://postgres.ai/console/gitlab/gitlab-production-tunnel-pg12/sessions/13176/commands/46201
Query
SELECT 1 AS one
FROM "namespace_settings"
WHERE "namespace_settings"."namespace_id" IN (WITH RECURSIVE "base_and_ancestors" AS ((
SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id",
"namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description",
"namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level",
"namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at",
"namespaces"."description_html", "namespaces"."lfs_enabled",
"namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version",
"namespaces"."project_creation_level", "namespaces"."runners_token",
"namespaces"."file_template_project_id", "namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id",
"namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level", "namespaces"."emails_disabled", "namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled", "namespaces"."default_branch_protection",
"namespaces"."unlock_membership_to_ldap", "namespaces"."max_personal_access_token_lifetime",
"namespaces"."push_rule_id", "namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids"
FROM "namespaces"
WHERE "namespaces"."type" = 'Group'
AND "namespaces"."id" = 9988194)
UNION (
SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id",
"namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description",
"namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level",
"namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error",
"namespaces"."ldap_sync_last_update_at",
"namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at",
"namespaces"."description_html", "namespaces"."lfs_enabled",
"namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit",
"namespaces"."require_two_factor_authentication",
"namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version",
"namespaces"."project_creation_level", "namespaces"."runners_token",
"namespaces"."file_template_project_id", "namespaces"."saml_discovery_token",
"namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id",
"namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit",
"namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level",
"namespaces"."subgroup_creation_level", "namespaces"."emails_disabled", "namespaces"."max_pages_size",
"namespaces"."max_artifacts_size",
"namespaces"."mentions_disabled", "namespaces"."default_branch_protection",
"namespaces"."unlock_membership_to_ldap", "namespaces"."max_personal_access_token_lifetime",
"namespaces"."push_rule_id", "namespaces"."shared_runners_enabled",
"namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids"
FROM "namespaces", "base_and_ancestors"
WHERE "namespaces"."type" = 'Group'
AND "namespaces"."id" = "base_and_ancestors"."parent_id"))
SELECT "namespaces"."id"
FROM "base_and_ancestors" AS "namespaces")
AND "namespace_settings"."runner_registration_enabled" = FALSE
LIMIT 1
Query plan
Limit (cost=365.10..371.37 rows=1 width=4) (actual time=97.167..97.174 rows=0 loops=1)
Buffers: shared hit=6 read=21
I/O Timings: read=96.490 write=0.000
-> Nested Loop (cost=365.10..402.75 rows=6 width=4) (actual time=97.165..97.172 rows=0 loops=1)
Buffers: shared hit=6 read=21
I/O Timings: read=96.490 write=0.000
-> HashAggregate (cost=364.66..364.77 rows=11 width=4) (actual time=41.807..41.823 rows=3 loops=1)
Group Key: namespaces.id
Buffers: shared hit=4 read=11
I/O Timings: read=41.333 write=0.000
-> CTE Scan on base_and_ancestors namespaces (cost=364.31..364.53 rows=11 width=4) (actual time=22.328..41.799 rows=3 loops=1)
Buffers: shared hit=4 read=11
I/O Timings: read=41.333 write=0.000
CTE base_and_ancestors
-> Recursive Union (cost=0.56..364.31 rows=11 width=365) (actual time=22.322..41.778 rows=3 loops=1)
Buffers: shared hit=4 read=11
I/O Timings: read=41.333 write=0.000
-> Index Scan using namespaces_pkey on public.namespaces namespaces_1 (cost=0.56..3.58 rows=1 width=365) (actual time=22.250..22.255 rows=1 loops=1)
Index Cond: (namespaces_1.id = 9988194)
Filter: ((namespaces_1.type)::text = 'Group'::text)
Rows Removed by Filter: 0
Buffers: shared read=5
I/O Timings: read=22.095 write=0.000
-> Nested Loop (cost=0.56..36.05 rows=1 width=365) (actual time=6.468..6.469 rows=1 loops=3)
Buffers: shared hit=4 read=6
I/O Timings: read=19.239 write=0.000
-> WorkTable Scan on base_and_ancestors (cost=0.00..0.20 rows=10 width=4) (actual time=0.001..0.002 rows=1 loops=3)
I/O Timings: read=0.000 write=0.000
-> Index Scan using namespaces_pkey on public.namespaces namespaces_2 (cost=0.56..3.58 rows=1 width=365) (actual time=6.460..6.460 rows=1 loops=3)
Index Cond: (namespaces_2.id = base_and_ancestors.parent_id)
Filter: ((namespaces_2.type)::text = 'Group'::text)
Rows Removed by Filter: 0
Buffers: shared hit=4 read=6
I/O Timings: read=19.239 write=0.000
-> Index Scan using namespace_settings_pkey on public.namespace_settings (cost=0.43..3.45 rows=1 width=4) (actual time=18.444..18.444 rows=0 loops=3)
Index Cond: (namespace_settings.namespace_id = namespaces.id)
Filter: (NOT namespace_settings.runner_registration_enabled)
Rows Removed by Filter: 1
Buffers: shared hit=2 read=10
I/O Timings: read=55.157 write=0.000