mirror of https://github.com/go-gitea/gitea
Map OIDC groups to Orgs/Teams (#21441)
Fixes #19555 Test-Instructions: https://github.com/go-gitea/gitea/pull/21441#issuecomment-1419438000 This PR implements the mapping of user groups provided by OIDC providers to orgs teams in Gitea. The main part is a refactoring of the existing LDAP code to make it usable from different providers. Refactorings: - Moved the router auth code from module to service because of import cycles - Changed some model methods to take a `Context` parameter - Moved the mapping code from LDAP to a common location I've tested it with Keycloak but other providers should work too. The JSON mapping format is the same as for LDAP. ![grafik](https://user-images.githubusercontent.com/1666336/195634392-3fc540fc-b229-4649-99ac-91ae8e19df2d.png) --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>pull/22798/head^2
parent
2c6cc0b8c9
commit
e8186f1c0f
@ -0,0 +1,22 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth |
||||
|
||||
import ( |
||||
"code.gitea.io/gitea/modules/json" |
||||
"code.gitea.io/gitea/modules/log" |
||||
) |
||||
|
||||
func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) { |
||||
groupTeamMapping := make(map[string]map[string][]string) |
||||
if raw == "" { |
||||
return groupTeamMapping, nil |
||||
} |
||||
err := json.Unmarshal([]byte(raw), &groupTeamMapping) |
||||
if err != nil { |
||||
log.Error("Failed to unmarshal group team mapping: %v", err) |
||||
return nil, err |
||||
} |
||||
return groupTeamMapping, nil |
||||
} |
@ -0,0 +1,60 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/web/middleware" |
||||
) |
||||
|
||||
// Auth is a middleware to authenticate a web user
|
||||
func Auth(authMethod Method) func(*context.Context) { |
||||
return func(ctx *context.Context) { |
||||
if err := authShared(ctx, authMethod); err != nil { |
||||
log.Error("Failed to verify user: %v", err) |
||||
ctx.Error(http.StatusUnauthorized, "Verify") |
||||
return |
||||
} |
||||
if ctx.Doer == nil { |
||||
// ensure the session uid is deleted
|
||||
_ = ctx.Session.Delete("uid") |
||||
} |
||||
} |
||||
} |
||||
|
||||
// APIAuth is a middleware to authenticate an api user
|
||||
func APIAuth(authMethod Method) func(*context.APIContext) { |
||||
return func(ctx *context.APIContext) { |
||||
if err := authShared(ctx.Context, authMethod); err != nil { |
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func authShared(ctx *context.Context, authMethod Method) error { |
||||
var err error |
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if ctx.Doer != nil { |
||||
if ctx.Locale.Language() != ctx.Doer.Language { |
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) |
||||
} |
||||
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName |
||||
ctx.IsSigned = true |
||||
ctx.Data["IsSigned"] = ctx.IsSigned |
||||
ctx.Data["SignedUser"] = ctx.Doer |
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID |
||||
ctx.Data["SignedUserName"] = ctx.Doer.Name |
||||
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin |
||||
} else { |
||||
ctx.Data["SignedUserID"] = int64(0) |
||||
ctx.Data["SignedUserName"] = "" |
||||
} |
||||
return nil |
||||
} |
@ -1,94 +0,0 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ldap |
||||
|
||||
import ( |
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/organization" |
||||
user_model "code.gitea.io/gitea/models/user" |
||||
"code.gitea.io/gitea/modules/log" |
||||
) |
||||
|
||||
// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
|
||||
func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) { |
||||
var err error |
||||
if source.GroupsEnabled && source.GroupTeamMapRemoval { |
||||
// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
|
||||
removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache) |
||||
} |
||||
for orgName, teamNames := range ldapTeamAdd { |
||||
org, ok := orgCache[orgName] |
||||
if !ok { |
||||
org, err = organization.GetOrgByName(orgName) |
||||
if err != nil { |
||||
// organization must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err) |
||||
continue |
||||
} |
||||
orgCache[orgName] = org |
||||
} |
||||
|
||||
for _, teamName := range teamNames { |
||||
team, ok := teamCache[orgName+teamName] |
||||
if !ok { |
||||
team, err = org.GetTeam(teamName) |
||||
if err != nil { |
||||
// team must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err) |
||||
continue |
||||
} |
||||
teamCache[orgName+teamName] = team |
||||
} |
||||
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); !isMember && err == nil { |
||||
log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name) |
||||
} else { |
||||
continue |
||||
} |
||||
err := models.AddTeamMember(team, user.ID) |
||||
if err != nil { |
||||
log.Error("LDAP group sync: Could not add user to team: %v", err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// remove membership to organizations/teams if user is not member of corresponding LDAP group
|
||||
// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y"
|
||||
// then users membership gets removed for all organizations/teams mapped by LDAP group "y"
|
||||
func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) { |
||||
var err error |
||||
for orgName, teamNames := range ldapTeamRemove { |
||||
org, ok := orgCache[orgName] |
||||
if !ok { |
||||
org, err = organization.GetOrgByName(orgName) |
||||
if err != nil { |
||||
// organization must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err) |
||||
continue |
||||
} |
||||
orgCache[orgName] = org |
||||
} |
||||
for _, teamName := range teamNames { |
||||
team, ok := teamCache[orgName+teamName] |
||||
if !ok { |
||||
team, err = org.GetTeam(teamName) |
||||
if err != nil { |
||||
// team must must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err) |
||||
continue |
||||
} |
||||
} |
||||
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); isMember && err == nil { |
||||
log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name) |
||||
} else { |
||||
continue |
||||
} |
||||
err = models.RemoveTeamMember(team, user.ID) |
||||
if err != nil { |
||||
log.Error("LDAP group sync: Could not remove user from team: %v", err) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,116 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package source |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"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" |
||||
) |
||||
|
||||
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] = teams |
||||
} |
||||
} else { |
||||
for org, teams := range memberships { |
||||
membershipsToRemove[org] = teams |
||||
} |
||||
} |
||||
} |
||||
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 := models.AddTeamMember(team, user.ID); err != nil { |
||||
log.Error("group sync: Could not add user to team: %v", err) |
||||
return err |
||||
} |
||||
} else if action == syncRemove && isMember { |
||||
if err := models.RemoveTeamMember(team, user.ID); err != nil { |
||||
log.Error("group sync: Could not remove user from team: %v", err) |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
Loading…
Reference in new issue