mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-28 01:42:52 +02:00
Even with the add/remove dedup in place, a user who is no longer in any group that maps to Owners but is the sole owner of the org still hits ErrLastOrgOwner during sync, which aborts OAuth login. Catch that one error in syncGroupsToTeamsCached, log a warning, and continue — the org invariant is preserved, login succeeds, and the next sync converges after an admin promotes another owner. Also iterate membershipsToRemove (not membershipsToAdd) in the dedup loop so we skip set allocation when an org has no removes, and add a dedicated test plus a post-sync owner-membership assertion in the existing test. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
144 lines
4.9 KiB
Go
144 lines
4.9 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package source
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"code.gitea.io/gitea/models/organization"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/log"
|
|
org_service "code.gitea.io/gitea/services/org"
|
|
)
|
|
|
|
type syncType int
|
|
|
|
const (
|
|
syncAdd syncType = iota
|
|
syncRemove
|
|
)
|
|
|
|
// SyncGroupsToTeams maps authentication source groups to organization and team memberships
|
|
func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool) error {
|
|
orgCache := make(map[string]*organization.Organization)
|
|
teamCache := make(map[string]*organization.Team)
|
|
return SyncGroupsToTeamsCached(ctx, user, sourceUserGroups, sourceGroupTeamMapping, performRemoval, orgCache, teamCache)
|
|
}
|
|
|
|
// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships
|
|
func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
|
|
membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping)
|
|
|
|
if performRemoval {
|
|
if err := syncGroupsToTeamsCached(ctx, user, membershipsToRemove, syncRemove, orgCache, teamCache); err != nil {
|
|
return fmt.Errorf("could not sync[remove] user groups: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := syncGroupsToTeamsCached(ctx, user, membershipsToAdd, syncAdd, orgCache, teamCache); err != nil {
|
|
return fmt.Errorf("could not sync[add] user groups: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resolveMappedMemberships(sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string) (map[string][]string, map[string][]string) {
|
|
membershipsToAdd := map[string][]string{}
|
|
membershipsToRemove := map[string][]string{}
|
|
for group, memberships := range sourceGroupTeamMapping {
|
|
isUserInGroup := sourceUserGroups.Contains(group)
|
|
if isUserInGroup {
|
|
for org, teams := range memberships {
|
|
membershipsToAdd[org] = append(membershipsToAdd[org], teams...)
|
|
}
|
|
} else {
|
|
for org, teams := range memberships {
|
|
membershipsToRemove[org] = append(membershipsToRemove[org], teams...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If another group grants the same team, don't remove it — for the Owners
|
|
// team this would fail with ErrLastOrgOwner before the add runs.
|
|
for org, removeTeams := range membershipsToRemove {
|
|
addTeams, ok := membershipsToAdd[org]
|
|
if !ok {
|
|
continue
|
|
}
|
|
addSet := container.SetOf(addTeams...)
|
|
filtered := make([]string, 0, len(removeTeams))
|
|
for _, team := range removeTeams {
|
|
if !addSet.Contains(team) {
|
|
filtered = append(filtered, team)
|
|
}
|
|
}
|
|
if len(filtered) > 0 {
|
|
membershipsToRemove[org] = filtered
|
|
} else {
|
|
delete(membershipsToRemove, org)
|
|
}
|
|
}
|
|
|
|
return membershipsToAdd, membershipsToRemove
|
|
}
|
|
|
|
func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeamMap map[string][]string, action syncType, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
|
|
for orgName, teamNames := range orgTeamMap {
|
|
var err error
|
|
org, ok := orgCache[orgName]
|
|
if !ok {
|
|
org, err = organization.GetOrgByName(ctx, orgName)
|
|
if err != nil {
|
|
if organization.IsErrOrgNotExist(err) {
|
|
// organization must be created before group sync
|
|
log.Warn("group sync: Could not find organisation %s: %v", orgName, err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
orgCache[orgName] = org
|
|
}
|
|
for _, teamName := range teamNames {
|
|
team, ok := teamCache[orgName+teamName]
|
|
if !ok {
|
|
team, err = org.GetTeam(ctx, teamName)
|
|
if err != nil {
|
|
if organization.IsErrTeamNotExist(err) {
|
|
// team must be created before group sync
|
|
log.Warn("group sync: Could not find team %s: %v", teamName, err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
teamCache[orgName+teamName] = team
|
|
}
|
|
|
|
isMember, err := organization.IsTeamMember(ctx, org.ID, team.ID, user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if action == syncAdd && !isMember {
|
|
if err := org_service.AddTeamMember(ctx, team, user); err != nil {
|
|
log.Error("group sync: Could not add user to team: %v", err)
|
|
return err
|
|
}
|
|
} else if action == syncRemove && isMember {
|
|
if err := org_service.RemoveTeamMember(ctx, team, user); err != nil {
|
|
// Skip sole-owner removal so OAuth login still succeeds.
|
|
if organization.IsErrLastOrgOwner(err) {
|
|
log.Warn("group sync: Skipping removal of last owner in org %s for user %s: %v", org.Name, user.Name, err)
|
|
continue
|
|
}
|
|
log.Error("group sync: Could not remove user from team: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|