Add virtual registries setting policy and update service
What does this MR do and why?
This MR adds a policy for the virtual registries setting and a create or update service that calls this policy.
The policy allows users that have admin_group permissions to read and update the virtual registries setting, if the group is a root group.
Database review details
This create_or_update services calls the method create_or_update_by! that has been added to the model. This calls find_or_intialize_by and updates the setting.
The above has been simplified, create_or_update services calls the scope for_group. This calls find_or_intialize_by which returns the existing setting or a new instance, then update! is called. The risk of concurrent requests occurring is low for this table, only one setting can be created per group and only a group owner can update.
The below query plans are still applicable.
Queries
Find:
SELECT "virtual_registries_settings".* FROM "virtual_registries_settings" WHERE "virtual_registries_settings"."group_id" = 2 LIMIT 1;
Insert:
INSERT INTO "virtual_registries_settings" ("group_id", "enabled", "created_at", "updated_at") VALUES (2, true, '2025-08-28 04:29:04.123456', '2025-08-28 04:29:04.123456') RETURNING "id";
Update:
UPDATE "virtual_registries_settings" SET "enabled" = false, "updated_at" = '2025-08-28 04:29:04.123456' WHERE "group_id" = 2;
Explain plan
Find: Limit (cost=0.15..3.17 rows=1 width=33) (actual time=0.031..0.031 rows=0 loops=1)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_virtual_registries_settings_on_group_id on public.virtual_registries_settings (cost=0.15..3.17 rows=1 width=33) (actual time=0.030..0.030 rows=0 loops=1)
Index Cond: (virtual_registries_settings.group_id = 2)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', work_mem = '100MB', random_page_cost = '1.5', jit = 'off', effective_cache_size = '472585MB'
https://postgres.ai/console/gitlab/gitlab-production-main/sessions/42891/commands/131315
Insert:
ModifyTable on public.virtual_registries_settings (cost=0.00..0.01 rows=1 width=33) (actual time=0.234..0.235 rows=1 loops=1)
Buffers: shared hit=66 dirtied=2
WAL: records=4 fpi=1 bytes=384
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.00..0.01 rows=1 width=33) (actual time=0.090..0.091 rows=1 loops=1)
Buffers: shared hit=16 dirtied=1
WAL: records=1 fpi=0 bytes=99
I/O Timings: read=0.000 write=0.000
Trigger RI_ConstraintTrigger_c_1312386350 for constraint fk_rails_a1646a6b7a: time=5.776 calls=1
Settings: effective_cache_size = '472585MB', seq_page_cost = '4', work_mem = '100MB', random_page_cost = '1.5', jit = 'off'
https://postgres.ai/console/gitlab/gitlab-production-main/sessions/42891/commands/131316
Update:
ModifyTable on public.virtual_registries_settings (cost=0.15..3.17 rows=0 width=0) (actual time=0.047..0.048 rows=0 loops=1)
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_virtual_registries_settings_on_group_id on public.virtual_registries_settings (cost=0.15..3.17 rows=1 width=15) (actual time=0.046..0.046 rows=0 loops=1)
Index Cond: (virtual_registries_settings.group_id = 2)
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', work_mem = '100MB', random_page_cost = '1.5', jit = 'off', effective_cache_size = '472585MB'
https://postgres.ai/console/gitlab/gitlab-production-main/sessions/42891/commands/131317
References
Screenshots or screen recordings
n/a
How to set up and validate locally
Validate policy:
Setup
-
Run
gdk rails c -
Create a group and a subgroup:.
[1] pry(main)> parent_group = FactoryBot.create(:group, :private, path: "parent_group")
=> #<Group id:104 @parent_group>
[2] pry(main)> sub_group = FactoryBot.create(:group, :private, parent: parent_group, path: "subgroup")
=> #<Group id:105 @parent_group/subgroup>
- Create users for parent group:
[6] pry(main)> non_group_member, external, guest, reporter, developer, maintainer, owner = [
[:user, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, :external, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, guest_of: parent_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, reporter_of: parent_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, developer_of: parent_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, maintainer_of: parent_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, owner_of: parent_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"]
].map { |args| FactoryBot.create(*args) }
=> [#<User id:75 @user_0f6f88fd>,
#<User id:76 @user_7077efc7>,
#<User id:77 @user_6c909d88>,
#<User id:78 @user_d8e6ff7e>,
#<User id:79 @user_65b3498f>,
#<User id:80 @user_e5a6ea69>,
#<User id:81 @user_627d15b2>]
- Create users for subgroup:
non_group_member_sub, external_sub, guest_sub, reporter_sub, developer_sub, maintainer_sub, owner_sub = [
[:user, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, :external, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, guest_of: sub_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, reporter_of: sub_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, developer_of: sub_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, maintainer_of: sub_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"],
[:user, owner_of: sub_group, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com"]
].map { |args| FactoryBot.create(*args) }
=> [#<User id:82 @user_4ca5eb16>,
#<User id:83 @user_6b9cc324>,
#<User id:84 @user_2080e121>,
#<User id:85 @user_c7bfba3d>,
#<User id:86 @user_eef0a487>,
#<User id:87 @user_552a4264>,
#<User id:88 @user_d9e1abdd>]
Test policy on root group:
- Only owner is allowed:
[13] pry(main)> virtual_registry_group = ::VirtualRegistries::Packages::Policies::Group.new(parent_group)
=> #<VirtualRegistries::Packages::Policies::Group:0x0000000332718a38
@group=#<Group id:104 @parent_group>>
[14] pry(main)> Ability.allowed?(non_group_member, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[16] pry(main)> Ability.allowed?(external, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[17] pry(main)> Ability.allowed?(guest, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[18] pry(main)> Ability.allowed?(reporter, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[19] pry(main)> Ability.allowed?(developer, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[20] pry(main)> Ability.allowed?(maintainer, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[21] pry(main)> Ability.allowed?(owner, :admin_virtual_registries_setting, virtual_registry_group)
=> true
Test policy on subgroup:
- None are allowed
[31] pry(main)> virtual_registry_group = ::VirtualRegistries::Packages::Policies::Group.new(sub_group)
=> #<VirtualRegistries::Packages::Policies::Group:0x00000003331b25e8
@group=#<Group id:105 @parent_group_two>>
[32] pry(main)> Ability.allowed?(owner_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[31] pry(main)> Ability.allowed?(maintainer_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[30] pry(main)> Ability.allowed?(developer_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[28] pry(main)> Ability.allowed?(reporter_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[27] pry(main)> Ability.allowed?(guest_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[26] pry(main)> Ability.allowed?(external_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
[25] pry(main)> Ability.allowed?(non_group_member_sub, :admin_virtual_registries_setting, virtual_registry_group)
=> false
Create or update service:
On root group:
-
Run
gdk rails c -
Get a root group
Group.where(parent_id: nil).first
[2] pry(main)> group = Group.first
=> #<Group id:22 @toolbox>
- Get a user
user = User.first
[4] pry(main)> user = User.first
=> #<User id:1 @root>
- Add as owner of group
group.add_owner(user)
[7] pry(main)> group.add_owner(user)
=> #<GroupMember:0x0000000318e1b6b0
id: 1,
access_level: 50,
source_id: 22,
source_type: "Namespace",
user_id: 1,
notification_level: 3,
type: "GroupMember",
created_at: Tue, 26 Aug 2025 00:30:34.126476000 UTC +00:00,
updated_at: Tue, 26 Aug 2025 00:30:34.126476000 UTC +00:00,
created_by_id: nil,
invite_email: nil,
invite_token: nil,
invite_accepted_at: nil,
requested_at: nil,
expires_at: nil,
ldap: false,
override: false,
state: 0,
invite_email_success: true,
member_namespace_id: 22,
member_role_id: nil,
expiry_notified_at: nil,
request_accepted_at: nil,
is_source_accessible_to_current_user: true>
- Run
VirtualRegistries::Settings::CreateOrUpdateService.new(group:, current_user: user, params: {:enabled => true}).execute
[11] pry(main)> VirtualRegistries::Settings::CreateOrUpdateService.new(group:, current_user: user, params: {:enabled => true}).execute
=> #<ServiceResponse:0x00000003265de8e0
@http_status=:ok,
@message=nil,
@payload=
{:virtual_registries_setting=>
#<VirtualRegistries::Setting:0x00000003225d7e90
id: 1,
group_id: 22,
created_at: Thu, 28 Aug 2025 07:16:37.215638000 UTC +00:00,
updated_at: Thu, 28 Aug 2025 07:16:37.215638000 UTC +00:00,
enabled: true>},
@reason=nil,
@status=:success>
- Run
VirtualRegistries::Setting.for_group(group)to confirm entry.
[17] pry(main)> VirtualRegistries::Setting.for_group(group)
VirtualRegistries::Setting Load (0.3ms) SELECT "virtual_registries_settings".* FROM "virtual_registries_settings" WHERE "virtual_registries_settings"."group_id" = 22 /* loading for pp */ LIMIT 11 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:fmccawley--20250615-27FX9,console_username:fionamccawley,line:<internal:kernel>:187:in `loop'*/
=> [#<VirtualRegistries::Setting:0x00000003112da500
id: 1,
group_id: 22,
created_at: Thu, 28 Aug 2025 07:16:37.215638000 UTC +00:00,
updated_at: Thu, 28 Aug 2025 07:21:37.704329000 UTC +00:00,
enabled: true>]
- Update enabled to false
VirtualRegistries::Settings::CreateOrUpdateService.new(group:, current_user: user, params: {:enabled => false}).execute
[13] pry(main)> VirtualRegistries::Settings::CreateOrUpdateService.new(group:, current_user: user, params: {:enabled => false}).execute
=> #<ServiceResponse:0x00000003283966d0
@http_status=:ok,
@message=nil,
@payload=
{:virtual_registries_setting=>
#<VirtualRegistries::Setting:0x0000000319b57f90
id: 1,
group_id: 22,
created_at: Thu, 28 Aug 2025 07:16:37.215638000 UTC +00:00,
updated_at: Thu, 28 Aug 2025 07:18:10.256793000 UTC +00:00,
enabled: false>},
@reason=nil,
@status=:success>
On subgroup:
- Create a subgroup:
[1] pry(main)> group = Group.where(parent_id: nil).first
=> #<Group id:22 @toolbox>
[3] pry(main)> subgroup = FactoryBot.create(:group, parent: group, path: "test_one")
=> #<Group id:120 @toolbox/test_one>
- Create a user and add as owner of subgroup
[4] pry(main)> subgroup_owner = FactoryBot.create(:user, owner_of: subgroup, username: "user_#{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com")
=> #<User id:89 @user_f787de35>
- Run
VirtualRegistries::Settings::CreateOrUpdateService.new(group: subgroup, current_user: subgroup_owner, params: {:enabled => true}).execute. This should return an unauthorized error.
[5] pry(main)> VirtualRegistries::Settings::CreateOrUpdateService.new(group: subgroup, current_user: subgroup_owner, params: {:enabled => true}).execute
=> #<ServiceResponse:0x0000000330831a98
@http_status=nil,
@message="Unauthorized",
@payload={},
@reason=:unauthorized,
@status=:error>
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.
Related to #554069 (closed)