Cache member roles assigned to users in projects
Goals
-
Create a table to record custom role assignment to users in projects through direct membership and project sharing (i.e. a group the user is a member of is added as member with a custom role to a project)
-
Enable
UserMemberRolesInProjectsPreloaderto also return a user's custom abilities in projects granted through project sharing using a simpler query compared to the current query used inUserMemberRolesInGroupsPreloader(shown below).Query to calculate user's custom abilities in groups through direct membership and group sharing
def union_query union_queries = [] member = Member.select('member_roles.permissions').with_user(user) group_member = member .joins(:member_role) .where(source_type: 'Namespace') .where('members.source_id IN (SELECT UNNEST(namespace_ids) as ids)') .to_sql if custom_role_for_group_link_enabled? group_link_join = member .joins('JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id') .where('group_group_links.shared_group_id IN (SELECT UNNEST(namespace_ids) as ids)') invited_member_role = group_link_join .joins('JOIN member_roles ON member_roles.id = group_group_links.member_role_id') .where('access_level > group_access') .to_sql # when both roles are custom roles with the same base access level, # choose the source role as the max role source_member_role = group_link_join .joins('JOIN member_roles ON member_roles.id = members.member_role_id') .where('(access_level < group_access) OR ' \ '(access_level = group_access AND group_group_links.member_role_id IS NOT NULL)') .to_sql union_queries.push(invited_member_role, source_member_role) end union_queries.push(group_member) union_queries.join(" UNION ALL ") endSELECT namespace_ids.namespace_id, custom_permissions.permissions FROM ( VALUES (67345893, ARRAY[65957873, 67345893]), (76931177, ARRAY[65957873, 76931177])) AS namespace_ids (namespace_id, namespace_ids), LATERAL ( SELECT "member_roles"."permissions" FROM "members" JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id JOIN member_roles ON member_roles.id = group_group_links.member_role_id WHERE "members"."user_id" = 24586657 AND (group_group_links.shared_group_id IN ( SELECT UNNEST(namespace_ids) AS ids)) AND (access_level > group_access) UNION ALL SELECT "member_roles"."permissions" FROM "members" JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id JOIN member_roles ON member_roles.id = members.member_role_id WHERE "members"."user_id" = 24586657 AND (group_group_links.shared_group_id IN ( SELECT UNNEST(namespace_ids) AS ids)) AND ((access_level < group_access) OR (access_level = group_access AND group_group_links.member_role_id IS NOT NULL)) UNION ALL SELECT "member_roles"."permissions" FROM "members" INNER JOIN "member_roles" ON "member_roles"."id" = "members"."member_role_id" WHERE "members"."user_id" = 24586657 AND "members"."source_type" = 'Namespace' AND (members.source_id IN ( SELECT UNNEST(namespace_ids) AS ids)) ) AS custom_permissionsThe new query will fetch
member_role_ids of the user for specific projects fromuser_project_member_rolestable and joining them withmember_rolesrecords to get the actual custom abilities. -
Follows the same caching implementation applied for
UserMemberRolesInGroupsPreloaderin Cache member roles assigned to users in groups (#513033) to prevent recurrence of Huge increase in DB primary CPU utilization (#507925 - closed) when Assign custom role when sharing a project to a ... (#468329) feature is globally enabled. -
Centralize the calculation logic for a user's effective member role in a shared group/project in
MemberRoleInShared*- This logic is complex (see https://docs.gitlab.com/user/custom_roles/#assign-a-custom-role-to-an-invited-group) and is currently duplicated across multiple areas (1, 2, 3) of our code base which increases the risk of inconsistencies when updates are made to one area but not the others.
Implementation plan
- Create
user_project_member_rolestable- records a member role assigned to a user in a project through
project membership- membership in another group invited to the project
- columns: user_id, project_id, member_role_id, shared_with_group_id
- purpose of shared_with_group_id column:
- exclude member roles assigned to a user via group sharing when feature flag is disabled
- determine which tables to join with (e.g. when
nil- member role is from project membership (direct or inherited), when present - member role is from group sharing) to determine the source of the member role
- records a member role assigned to a user in a project through
- Implement caching triggers (feature flag:
cache_user_project_member_roles)On project membership change: created, updated, destroyed- On project group link change: created, updated
- On project group link destroyed
On user accept invite e.g. email address is invited to a project with a memberOn user's access request granted
- Rollout caching
- Backfill
user_project_member_rolestable - Update
UserMemberRolesInProjectsPreloaderto useuser_project_member_rolestable
References
-
assign_custom_roles_to_project_links_saas- feature flag that enables assigning custom role to invited group in a project