Migrate member role abilities
What does this MR do and why?
This migrates individual custom role boolean permission columns to a single JSONB permissions
column.
The individual permission columns are ignored here and will be removed in 16.11.
Issue: #441449 (closed)
Database
MemberRoleAbilityLoader Query
Rails query:
Preloaders::UserMemberRolesInGroupsPreloader.new(groups: [Group.find(60357923)], user: User.find(4018056)).execute
Before
Raw SQL
SELECT
namespace_ids.namespace_id,
bool_or(custom_permissions.admin_cicd_variables) AS admin_cicd_variables,
bool_or(custom_permissions.admin_group_member) AS admin_group_member,
bool_or(custom_permissions.admin_vulnerability) AS admin_vulnerability,
bool_or(custom_permissions.manage_group_access_tokens) AS manage_group_access_tokens,
bool_or(custom_permissions.read_dependency) AS read_dependency,
bool_or(custom_permissions.read_vulnerability) AS read_vulnerability,
bool_or(custom_permissions.remove_group) AS remove_group
FROM (
VALUES(60357923, ARRAY [60357594,60357923]::integer [])) AS namespace_ids (namespace_id,
namespace_ids),
LATERAL ((
SELECT
admin_cicd_variables,
admin_group_member,
admin_vulnerability,
manage_group_access_tokens,
read_dependency,
read_vulnerability,
remove_group
FROM
"members"
LEFT OUTER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id"
WHERE (members.source_type = 'Namespace'
AND members.source_id = namespace_ids.namespace_id)
AND "members"."user_id" = 4018056
AND(member_roles.admin_cicd_variables = TRUE
OR member_roles.admin_group_member = TRUE
OR member_roles.admin_vulnerability = TRUE
OR member_roles.manage_group_access_tokens = TRUE
OR member_roles.read_dependency = TRUE
OR member_roles.read_vulnerability = TRUE
OR member_roles.remove_group = TRUE))
UNION ALL (
SELECT
admin_cicd_variables,
admin_group_member,
admin_vulnerability,
manage_group_access_tokens,
read_dependency,
read_vulnerability,
remove_group
FROM
"members"
LEFT OUTER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id"
WHERE (members.source_type = 'Namespace'
AND members.source_id IN(
SELECT
UNNEST(namespace_ids) AS ids))
AND "members"."user_id" = 4018056
AND(member_roles.admin_cicd_variables = TRUE
OR member_roles.admin_group_member = TRUE
OR member_roles.admin_vulnerability = TRUE
OR member_roles.manage_group_access_tokens = TRUE
OR member_roles.read_dependency = TRUE
OR member_roles.read_vulnerability = TRUE
OR member_roles.remove_group = TRUE))
UNION ALL (
SELECT
FALSE AS admin_cicd_variables,
FALSE AS admin_group_member,
FALSE AS admin_vulnerability,
FALSE AS manage_group_access_tokens,
FALSE AS read_dependency,
FALSE AS read_vulnerability,
FALSE AS remove_group)) AS custom_permissions
GROUP BY
namespace_ids.namespace_id;
Plan
Postgres.ai Aggregate (cost=0.84..14.08 rows=1 width=11) (actual time=9.225..9.240 rows=1 loops=1)
Group Key: 60357923
Buffers: shared hit=9 read=4
I/O Timings: read=8.928 write=0.000
-> Result (cost=0.84..14.01 rows=3 width=11) (actual time=9.215..9.230 rows=1 loops=1)
Buffers: shared hit=9 read=4
I/O Timings: read=8.928 write=0.000
-> Append (cost=0.84..13.98 rows=3 width=7) (actual time=9.213..9.227 rows=1 loops=1)
Buffers: shared hit=9 read=4
I/O Timings: read=8.928 write=0.000
-> Nested Loop (cost=0.84..6.89 rows=1 width=7) (actual time=6.825..6.835 rows=0 loops=1)
Buffers: shared hit=1 read=3
I/O Timings: read=6.723 write=0.000
-> Index Only Scan using idx_members_on_user_and_source_and_source_type_and_member_role on public.members (cost=0.56..3.59 rows=1 width=8) (actual time=6.823..6.824 rows=0 loops=1)
Index Cond: ((members.user_id = 971158) AND (members.source_id = 60357923) AND (members.source_type = 'Namespace'::text))
Heap Fetches: 0
Buffers: shared hit=1 read=3
I/O Timings: read=6.723 write=0.000
-> Index Scan using member_roles_pkey on public.member_roles (cost=0.28..3.29 rows=1 width=15) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (member_roles.id = members.member_role_id)
Filter: (member_roles.admin_cicd_variables OR member_roles.admin_group_member OR member_roles.admin_vulnerability OR member_roles.manage_group_access_tokens OR member_roles.read_dependency OR member_roles.read_vulnerability OR member_roles.remove_group)
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=0.90..7.05 rows=1 width=7) (actual time=2.381..2.383 rows=0 loops=1)
Buffers: shared hit=8 read=1
I/O Timings: read=2.205 write=0.000
-> Merge Semi Join (cost=0.62..4.00 rows=1 width=8) (actual time=2.318..2.324 rows=1 loops=1)
Merge Cond: (members_1.source_id = (unnest('{60357594,60357923}'::integer[])))
Buffers: shared hit=5 read=1
I/O Timings: read=2.205 write=0.000
-> Index Only Scan using idx_members_on_user_and_source_and_source_type_and_member_role on public.members members_1 (cost=0.56..3.90 rows=6 width=12) (actual time=2.253..2.256 rows=2 loops=1)
Index Cond: ((members_1.user_id = 971158) AND (members_1.source_type = 'Namespace'::text))
Heap Fetches: 0
Buffers: shared hit=5 read=1
I/O Timings: read=2.205 write=0.000
-> Sort (cost=0.06..0.06 rows=2 width=4) (actual time=0.058..0.059 rows=2 loops=1)
Sort Key: (unnest('{60357594,60357923}'::integer[]))
Sort Method: quicksort Memory: 25kB
I/O Timings: read=0.000 write=0.000
-> ProjectSet (cost=0.00..0.03 rows=2 width=4) (actual time=0.010..0.011 rows=2 loops=1)
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=1)
I/O Timings: read=0.000 write=0.000
-> Index Scan using member_roles_pkey on public.member_roles member_roles_1 (cost=0.28..3.04 rows=1 width=15) (actual time=0.053..0.053 rows=0 loops=1)
Index Cond: (member_roles_1.id = members_1.member_role_id)
Filter: (member_roles_1.admin_cicd_variables OR member_roles_1.admin_group_member OR member_roles_1.admin_vulnerability OR member_roles_1.manage_group_access_tokens OR member_roles_1.read_dependency OR member_roles_1.read_vulnerability OR member_roles_1.remove_group)
Rows Removed by Filter: 1
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.00..0.01 rows=1 width=7) (actual time=0.002..0.002 rows=1 loops=1)
I/O Timings: read=0.000 write=0.000
After
Raw SQL
SELECT
namespace_ids.namespace_id,
bool_or(custom_permissions.admin_cicd_variables) AS admin_cicd_variables,
bool_or(custom_permissions.admin_group_member) AS admin_group_member,
bool_or(custom_permissions.admin_vulnerability) AS admin_vulnerability,
bool_or(custom_permissions.manage_group_access_tokens) AS manage_group_access_tokens,
bool_or(custom_permissions.read_dependency) AS read_dependency,
bool_or(custom_permissions.read_vulnerability) AS read_vulnerability,
bool_or(custom_permissions.remove_group) AS remove_group
FROM (
VALUES(60357923, ARRAY [60357594,60357923]::integer [])) AS namespace_ids (namespace_id,
namespace_ids),
LATERAL ((
SELECT
(member_roles.permissions -> 'admin_cicd_variables')::BOOLEAN AS admin_cicd_variables,
(member_roles.permissions -> 'admin_group_member')::BOOLEAN AS admin_group_member,
(member_roles.permissions -> 'admin_vulnerability')::BOOLEAN AS admin_vulnerability,
(member_roles.permissions -> 'manage_group_access_tokens')::BOOLEAN AS manage_group_access_tokens,
(member_roles.permissions -> 'read_dependency')::BOOLEAN AS read_dependency,
(member_roles.permissions -> 'read_vulnerability')::BOOLEAN AS read_vulnerability,
(member_roles.permissions -> 'remove_group')::BOOLEAN AS remove_group
FROM
"members"
LEFT OUTER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id"
WHERE (members.source_type = 'Namespace'
AND members.source_id = namespace_ids.namespace_id)
AND "members"."user_id" = 4018056
AND(member_roles.permissions @> ('{"admin_cicd_variables":true}')::jsonb
OR member_roles.permissions @> ('{"admin_group_member":true}')::jsonb
OR member_roles.permissions @> ('{"admin_vulnerability":true}')::jsonb
OR member_roles.permissions @> ('{"manage_group_access_tokens":true}')::jsonb
OR member_roles.permissions @> ('{"read_dependency":true}')::jsonb
OR member_roles.permissions @> ('{"read_vulnerability":true}')::jsonb
OR member_roles.permissions @> ('{"remove_group":true}')::jsonb))
UNION ALL (
SELECT
(member_roles.permissions -> 'admin_cicd_variables')::BOOLEAN AS admin_cicd_variables,
(member_roles.permissions -> 'admin_group_member')::BOOLEAN AS admin_group_member,
(member_roles.permissions -> 'admin_vulnerability')::BOOLEAN AS admin_vulnerability,
(member_roles.permissions -> 'manage_group_access_tokens')::BOOLEAN AS manage_group_access_tokens,
(member_roles.permissions -> 'read_dependency')::BOOLEAN AS read_dependency,
(member_roles.permissions -> 'read_vulnerability')::BOOLEAN AS read_vulnerability,
(member_roles.permissions -> 'remove_group')::BOOLEAN AS remove_group
FROM
"members"
LEFT OUTER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id"
WHERE (members.source_type = 'Namespace'
AND members.source_id IN(
SELECT
UNNEST(namespace_ids) AS ids))
AND "members"."user_id" = 4018056
AND(member_roles.permissions @> ('{"admin_cicd_variables":true}')::jsonb
OR member_roles.permissions @> ('{"admin_group_member":true}')::jsonb
OR member_roles.permissions @> ('{"admin_vulnerability":true}')::jsonb
OR member_roles.permissions @> ('{"manage_group_access_tokens":true}')::jsonb
OR member_roles.permissions @> ('{"read_dependency":true}')::jsonb
OR member_roles.permissions @> ('{"read_vulnerability":true}')::jsonb
OR member_roles.permissions @> ('{"remove_group":true}')::jsonb))
UNION ALL (
SELECT
FALSE AS admin_cicd_variables,
FALSE AS admin_group_member,
FALSE AS admin_vulnerability,
FALSE AS manage_group_access_tokens,
FALSE AS read_dependency,
FALSE AS read_vulnerability,
FALSE AS remove_group)) AS custom_permissions
GROUP BY
namespace_ids.namespace_id;
Plan
Postgres.ai Aggregate (cost=0.84..14.18 rows=1 width=11) (actual time=0.144..0.147 rows=1 loops=1)
Group Key: 60357923
Buffers: shared hit=13
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.84..14.11 rows=3 width=11) (actual time=0.136..0.139 rows=1 loops=1)
Buffers: shared hit=13
I/O Timings: read=0.000 write=0.000
-> Append (cost=0.84..14.08 rows=3 width=7) (actual time=0.136..0.138 rows=1 loops=1)
Buffers: shared hit=13
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=0.84..6.94 rows=1 width=7) (actual time=0.036..0.037 rows=0 loops=1)
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using idx_members_on_user_and_source_and_source_type_and_member_role on public.members (cost=0.56..3.59 rows=1 width=8) (actual time=0.036..0.036 rows=0 loops=1)
Index Cond: ((members.user_id = 971158) AND (members.source_id = 60357923) AND (members.source_type = 'Namespace'::text))
Heap Fetches: 0
Buffers: shared hit=4
I/O Timings: read=0.000 write=0.000
-> Index Scan using member_roles_pkey on public.member_roles (cost=0.28..3.31 rows=1 width=354) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (member_roles.id = members.member_role_id)
Filter: ((member_roles.permissions @> '{"admin_cicd_variables": true}'::jsonb) OR (member_roles.permissions @> '{"admin_group_member": true}'::jsonb) OR (member_roles.permissions @> '{"admin_vulnerability": true}'::jsonb) OR (member_roles.permissions @> '{"manage_group_access_tokens": true}'::jsonb) OR (member_roles.permissions @> '{"read_dependency": true}'::jsonb) OR (member_roles.permissions @> '{"read_vulnerability": true}'::jsonb) OR (member_roles.permissions @> '{"remove_group": true}'::jsonb))
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=0.90..7.10 rows=1 width=7) (actual time=0.097..0.098 rows=0 loops=1)
Buffers: shared hit=9
I/O Timings: read=0.000 write=0.000
-> Merge Semi Join (cost=0.62..4.00 rows=1 width=8) (actual time=0.047..0.050 rows=1 loops=1)
Merge Cond: (members_1.source_id = (unnest('{60357594,60357923}'::integer[])))
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using idx_members_on_user_and_source_and_source_type_and_member_role on public.members members_1 (cost=0.56..3.90 rows=6 width=12) (actual time=0.018..0.019 rows=2 loops=1)
Index Cond: ((members_1.user_id = 971158) AND (members_1.source_type = 'Namespace'::text))
Heap Fetches: 0
Buffers: shared hit=6
I/O Timings: read=0.000 write=0.000
-> Sort (cost=0.06..0.06 rows=2 width=4) (actual time=0.027..0.028 rows=2 loops=1)
Sort Key: (unnest('{60357594,60357923}'::integer[]))
Sort Method: quicksort Memory: 25kB
I/O Timings: read=0.000 write=0.000
-> ProjectSet (cost=0.00..0.03 rows=2 width=4) (actual time=0.003..0.004 rows=2 loops=1)
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.000..0.001 rows=1 loops=1)
I/O Timings: read=0.000 write=0.000
-> Index Scan using member_roles_pkey on public.member_roles member_roles_1 (cost=0.28..3.06 rows=1 width=354) (actual time=0.046..0.046 rows=0 loops=1)
Index Cond: (member_roles_1.id = members_1.member_role_id)
Filter: ((member_roles_1.permissions @> '{"admin_cicd_variables": true}'::jsonb) OR (member_roles_1.permissions @> '{"admin_group_member": true}'::jsonb) OR (member_roles_1.permissions @> '{"admin_vulnerability": true}'::jsonb) OR (member_roles_1.permissions @> '{"manage_group_access_tokens": true}'::jsonb) OR (member_roles_1.permissions @> '{"read_dependency": true}'::jsonb) OR (member_roles_1.permissions @> '{"read_vulnerability": true}'::jsonb) OR (member_roles_1.permissions @> '{"remove_group": true}'::jsonb))
Rows Removed by Filter: 1
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Result (cost=0.00..0.01 rows=1 width=7) (actual time=0.001..0.001 rows=1 loops=1)
I/O Timings: read=0.000 write=0.000
Conclusion: the plans are similar, except for the filter condition on the index scan (performed in loop).
Note there is a maximum of 10 member roles per namespace and there are currently < 1000 records in GitLab.com production database.
How to set up and validate locally
Before checking out this branch
- Create a
MemberRole
withread_code
permissions in the consoleMemberRole.create(base_access_level: 10, read_code: true)
- Check out this branch and run the migrations
- Select the latest
MemberRole
in the console and verify theread_code
permission is enabledMemberRole.last.read_code == true
Edited by Alex Buijs