package service import ( "context" "strings" "go.infratographer.com/x/gidx" "golang.org/x/exp/slices" "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" ) // processMemberships determines the changes between what is wanted and what is live and executes on the differences. // If skipDeletions is true, no deletes will be executed. func (s *service) processMemberships(ctx context.Context, relationships Relationships, skipDeletions bool) (int, int) { if len(relationships.Memberships) == 0 { return 0, 0 } rlogger := s.logger.With("resource.id", relationships.Resource.PrefixedID()) roleIDs := make(map[string]gidx.PrefixedID) var ( totalRoleCreate, totalRoleDelete int totalRoleAssign, totalRoleUnassign int ) wantRoles, wantAssignments := s.mapResourceWants(relationships.Memberships) liveRoles, liveAssignments, err := s.mapResourceDetails(ctx, relationships.Resource.PrefixedID()) if err != nil { rlogger.Errorw("failed to get membership resource details map", "error", err, ) return 0, 0 } roleCreations := make(map[string][]string) roleDeletions := make([]gidx.PrefixedID, 0) for roleKey, actions := range wantRoles { if _, ok := liveRoles[roleKey]; !ok { roleCreations[roleKey] = actions } } for roleKey, role := range liveRoles { if !skipDeletions { if _, ok := wantRoles[roleKey]; !ok { roleDeletions = append(roleDeletions, role.ID) } } roleIDs[roleKey] = role.ID } roleMembershipsAdd := make(map[string][]gidx.PrefixedID) roleMembershipsRemove := make(map[string][]gidx.PrefixedID) for roleKey, assignments := range wantAssignments { for memberID := range assignments { if _, ok := liveAssignments[roleKey]; ok { if _, ok := liveAssignments[roleKey][memberID]; ok { continue } } roleMembershipsAdd[roleKey] = append(roleMembershipsAdd[roleKey], memberID) totalRoleAssign++ } } if !skipDeletions { for roleKey, assignments := range liveAssignments { for memberID := range assignments { if _, ok := wantAssignments[roleKey]; ok { if _, ok := wantAssignments[roleKey][memberID]; ok { continue } } roleMembershipsRemove[roleKey] = append(roleMembershipsRemove[roleKey], memberID) totalRoleUnassign++ } } } totalRoleCreate += len(roleCreations) totalRoleDelete += len(roleDeletions) rlogger.Debugw("processing memberships", "role.create", totalRoleCreate, "role.delete", totalRoleDelete, "role.assign", totalRoleAssign, "role.unassign", totalRoleUnassign, ) var ( rolesCreated, rolesDeleted int roleAssignments, roleUnassignments int ) for roleKey, actions := range roleCreations { roleID, err := s.perms.CreateRole(ctx, relationships.Resource.PrefixedID(), actions) if err != nil { rlogger.Errorw("error creating role", "actions", actions, "error", err) continue } roleIDs[roleKey] = roleID rolesCreated++ } for _, roleID := range roleDeletions { if err := s.perms.DeleteRole(ctx, roleID); err != nil { rlogger.Errorw("error deleting role", "role.id", roleID, "error", err) continue } rolesDeleted++ } for roleKey, members := range roleMembershipsAdd { roleID, ok := roleIDs[roleKey] if !ok { rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) continue } for _, memberID := range members { if err := s.perms.AssignRole(ctx, roleID, memberID); err != nil { rlogger.Errorw("error assigning member to role", "role.id", roleID, "member.id", memberID, "error", err) continue } roleAssignments++ } } for roleKey, members := range roleMembershipsRemove { roleID, ok := roleIDs[roleKey] if !ok { rlogger.Errorw("role id not found for role actions key", "role_actions_key", roleKey, "members", len(members)) continue } for _, memberID := range members { if err := s.perms.UnassignRole(ctx, roleID, memberID); err != nil { rlogger.Errorw("error removing member from role", "role.id", roleID, "member.id", memberID, "error", err) continue } roleUnassignments++ } } rlogger.Debugw("memberships processed", "role.create", rolesCreated, "role.delete", rolesDeleted, "role.assign", roleAssignments, "role.unassign", roleUnassignments, ) return rolesCreated + rolesDeleted, roleAssignments + roleUnassignments } // mapResourceWants processes the provided memberships and returns two maps. // A Role Key is computed based on a sorted slice of actions for each role. // The first map is of Role Key -> list of actions // The second map is of Role Key -> Member ID -> true func (s *service) mapResourceWants(memberships []ResourceMemberships) (map[string][]string, map[string]map[gidx.PrefixedID]bool) { roleActionsKey := make(map[string]string) for role, actions := range s.roles { roleActionsKey[role] = strings.Join(actions, "|") } wantRoles := make(map[string][]string) wantAssignments := make(map[string]map[gidx.PrefixedID]bool) for _, membership := range memberships { roleKey := roleActionsKey[membership.Role] if _, ok := wantRoles[roleKey]; !ok { wantRoles[roleKey] = s.roles[membership.Role] wantAssignments[roleKey] = make(map[gidx.PrefixedID]bool) } wantAssignments[roleKey][membership.Member.PrefixedID()] = true } return wantRoles, wantAssignments } // mapResourceDetails fetches the provided ResourceID's live state and returns two maps and an error. // A Role Key is computed based on a sorted slice of actions for each role. // The first map is of Role Key -> Permissions Resource Role // The second map is of Role Key -> Member ID -> true func (s *service) mapResourceDetails(ctx context.Context, resourceID gidx.PrefixedID) (map[string]permissions.ResourceRole, map[string]map[gidx.PrefixedID]bool, error) { roles := make(map[string]permissions.ResourceRole) assignments := make(map[string]map[gidx.PrefixedID]bool) liveRoles, err := s.perms.ListResourceRoles(ctx, resourceID) if err != nil { return nil, nil, err } for _, role := range liveRoles { slices.Sort(role.Actions) roleKey := strings.Join(role.Actions, "|") roles[roleKey] = role liveAssignments, err := s.perms.ListRoleAssignments(ctx, role.ID) if err != nil { return nil, nil, err } assignments[roleKey] = make(map[gidx.PrefixedID]bool) for _, assignment := range liveAssignments { assignments[roleKey][assignment] = true } } return roles, assignments, nil }