aboutsummaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
authorKN4CK3R2023-02-08 07:44:42 +0100
committerGitHub2023-02-08 14:44:42 +0800
commite8186f1c0f194ce3f63bed9a564002b80c0859c9 (patch)
tree75ffc50f54af2ef441ecf60448531b9e0ed64490 /services
parent2c6cc0b8c982b3d49a5b208f75e15b2269584312 (diff)
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>
Diffstat (limited to 'services')
-rw-r--r--services/auth/middleware.go60
-rw-r--r--services/auth/source/ldap/source_authenticate.go96
-rw-r--r--services/auth/source/ldap/source_group_sync.go94
-rw-r--r--services/auth/source/ldap/source_search.go136
-rw-r--r--services/auth/source/ldap/source_sync.go11
-rw-r--r--services/auth/source/oauth2/source.go23
-rw-r--r--services/auth/source/source_group_sync.go116
-rw-r--r--services/forms/auth_form.go4
8 files changed, 294 insertions, 246 deletions
diff --git a/services/auth/middleware.go b/services/auth/middleware.go
new file mode 100644
index 000000000..cccaab299
--- /dev/null
+++ b/services/auth/middleware.go
@@ -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
+}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 321cf5540..fba8da793 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -10,9 +10,10 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
+ auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/util"
+ source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
)
@@ -64,61 +65,66 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
}
if user != nil {
- if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- orgCache := make(map[string]*organization.Organization)
- teamCache := make(map[string]*organization.Team)
- source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
- }
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) {
- return user, asymkey_model.RewriteAllPublicKeys()
+ if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
+ return user, err
+ }
+ }
+ } else {
+ // Fallback.
+ if len(sr.Username) == 0 {
+ sr.Username = userName
}
- return user, nil
- }
-
- // Fallback.
- if len(sr.Username) == 0 {
- sr.Username = userName
- }
- if len(sr.Mail) == 0 {
- sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
- }
+ if len(sr.Mail) == 0 {
+ sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
+ }
- user = &user_model.User{
- LowerName: strings.ToLower(sr.Username),
- Name: sr.Username,
- FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
- Email: sr.Mail,
- LoginType: source.authSource.Type,
- LoginSource: source.authSource.ID,
- LoginName: userName,
- IsAdmin: sr.IsAdmin,
- }
- overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
- IsActive: util.OptionalBoolTrue,
- }
+ user = &user_model.User{
+ LowerName: strings.ToLower(sr.Username),
+ Name: sr.Username,
+ FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
+ Email: sr.Mail,
+ LoginType: source.authSource.Type,
+ LoginSource: source.authSource.ID,
+ LoginName: userName,
+ IsAdmin: sr.IsAdmin,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
+ IsActive: util.OptionalBoolTrue,
+ }
- err := user_model.CreateUser(user, overwriteDefault)
- if err != nil {
- return user, err
- }
+ err := user_model.CreateUser(user, overwriteDefault)
+ if err != nil {
+ return user, err
+ }
- mailer.SendRegisterNotifyMail(user)
+ mailer.SendRegisterNotifyMail(user)
- if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
- err = asymkey_model.RewriteAllPublicKeys()
- }
- if err == nil && len(source.AttributeAvatar) > 0 {
- _ = user_service.UploadAvatar(user, sr.Avatar)
+ if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
+ if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
+ return user, err
+ }
+ }
+ if len(source.AttributeAvatar) > 0 {
+ if err := user_service.UploadAvatar(user, sr.Avatar); err != nil {
+ return user, err
+ }
+ }
}
+
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- orgCache := make(map[string]*organization.Organization)
- teamCache := make(map[string]*organization.Team)
- source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
+ groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
+ if err != nil {
+ return user, err
+ }
+ if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
+ return user, err
+ }
}
- return user, err
+ return user, nil
}
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go
deleted file mode 100644
index 95a608492..000000000
--- a/services/auth/source/ldap/source_group_sync.go
+++ /dev/null
@@ -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)
- }
- }
- }
-}
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 16f13029f..5a2d25b0c 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -11,26 +11,24 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
"github.com/go-ldap/ldap/v3"
)
// SearchResult : user data
type SearchResult struct {
- Username string // Username
- Name string // Name
- Surname string // Surname
- Mail string // E-mail address
- SSHPublicKey []string // SSH Public Key
- IsAdmin bool // if user is administrator
- IsRestricted bool // if user is restricted
- LowerName string // LowerName
- Avatar []byte
- LdapTeamAdd map[string][]string // organizations teams to add
- LdapTeamRemove map[string][]string // organizations teams to remove
+ Username string // Username
+ Name string // Name
+ Surname string // Surname
+ Mail string // E-mail address
+ SSHPublicKey []string // SSH Public Key
+ IsAdmin bool // if user is administrator
+ IsRestricted bool // if user is restricted
+ LowerName string // LowerName
+ Avatar []byte
+ Groups container.Set[string]
}
func (source *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -196,9 +194,8 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
}
// List all group memberships of a user
-func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
- var ldapGroups []string
- var searchFilter string
+func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] {
+ ldapGroups := make(container.Set[string])
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
if !ok {
@@ -210,12 +207,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
return ldapGroups
}
+ var searchFilter string
if applyGroupFilter {
searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
} else {
searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
}
-
result, err := l.Search(ldap.NewSearchRequest(
groupDN,
ldap.ScopeWholeSubtree,
@@ -237,44 +234,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
log.Error("LDAP search was successful, but found no DN!")
continue
}
- ldapGroups = append(ldapGroups, entry.DN)
+ ldapGroups.Add(entry.DN)
}
return ldapGroups
}
-// parse LDAP groups and return map of ldap groups to organizations teams
-func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
- ldapGroupsToTeams := make(map[string]map[string][]string)
- err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams)
- if err != nil {
- log.Error("Failed to unmarshall LDAP teams map: %v", err)
- return ldapGroupsToTeams
- }
- return ldapGroupsToTeams
-}
-
-// getMappedMemberships : returns the organizations and teams to modify the users membership
-func (source *Source) getMappedMemberships(usersLdapGroups []string, uid string) (map[string][]string, map[string][]string) {
- // unmarshall LDAP group team map from configs
- ldapGroupsToTeams := source.mapLdapGroupsToTeams()
- membershipsToAdd := map[string][]string{}
- membershipsToRemove := map[string][]string{}
- for group, memberships := range ldapGroupsToTeams {
- isUserInGroup := util.SliceContainsString(usersLdapGroups, group)
- if isUserInGroup {
- for org, teams := range memberships {
- membershipsToAdd[org] = teams
- }
- } else if !isUserInGroup {
- for org, teams := range memberships {
- membershipsToRemove[org] = teams
- }
- }
- }
- return membershipsToAdd, membershipsToRemove
-}
-
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
if strings.ToLower(source.UserUID) == "dn" {
return entry.DN
@@ -399,23 +364,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
- teamsToAdd := make(map[string][]string)
- teamsToRemove := make(map[string][]string)
-
- // Check group membership
- if source.GroupsEnabled {
- userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
- usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
-
- if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
- return nil
- }
-
- if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
- teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
- }
- }
-
if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
}
@@ -431,6 +379,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
}
+ // Check group membership
+ var usersLdapGroups container.Set[string]
+ if source.GroupsEnabled {
+ userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
+ usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
+
+ if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
+ return nil
+ }
+ }
+
if !directBind && source.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
@@ -440,17 +399,16 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
}
return &SearchResult{
- LowerName: strings.ToLower(username),
- Username: username,
- Name: firstname,
- Surname: surname,
- Mail: mail,
- SSHPublicKey: sshPublicKey,
- IsAdmin: isAdmin,
- IsRestricted: isRestricted,
- Avatar: Avatar,
- LdapTeamAdd: teamsToAdd,
- LdapTeamRemove: teamsToRemove,
+ LowerName: strings.ToLower(username),
+ Username: username,
+ Name: firstname,
+ Surname: surname,
+ Mail: mail,
+ SSHPublicKey: sshPublicKey,
+ IsAdmin: isAdmin,
+ IsRestricted: isRestricted,
+ Avatar: Avatar,
+ Groups: usersLdapGroups,
}
}
@@ -512,33 +470,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
result := make([]*SearchResult, 0, len(sr.Entries))
for _, v := range sr.Entries {
- teamsToAdd := make(map[string][]string)
- teamsToRemove := make(map[string][]string)
-
+ var usersLdapGroups container.Set[string]
if source.GroupsEnabled {
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
if source.GroupFilter != "" {
- usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
+ usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
if len(usersLdapGroups) == 0 {
continue
}
}
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
- usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
- teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
+ usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
}
}
user := &SearchResult{
- Username: v.GetAttributeValue(source.AttributeUsername),
- Name: v.GetAttributeValue(source.AttributeName),
- Surname: v.GetAttributeValue(source.AttributeSurname),
- Mail: v.GetAttributeValue(source.AttributeMail),
- IsAdmin: checkAdmin(l, source, v.DN),
- LdapTeamAdd: teamsToAdd,
- LdapTeamRemove: teamsToRemove,
+ Username: v.GetAttributeValue(source.AttributeUsername),
+ Name: v.GetAttributeValue(source.AttributeName),
+ Surname: v.GetAttributeValue(source.AttributeSurname),
+ Mail: v.GetAttributeValue(source.AttributeMail),
+ IsAdmin: checkAdmin(l, source, v.DN),
+ Groups: usersLdapGroups,
}
if !user.IsAdmin {
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 73e8309ac..4571ff654 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -13,8 +13,10 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
+ auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
+ source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
)
@@ -65,6 +67,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
orgCache := make(map[string]*organization.Organization)
teamCache := make(map[string]*organization.Team)
+ groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
+ if err != nil {
+ return err
+ }
+
for _, su := range sr {
select {
case <-ctx.Done():
@@ -173,7 +180,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
// Synchronize LDAP groups with organization and team memberships
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
- source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache)
+ if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil {
+ log.Error("SyncGroupsToTeamsCached: %v", err)
+ }
}
}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 0abebc04e..675005e55 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -8,13 +8,6 @@ import (
"code.gitea.io/gitea/modules/json"
)
-// ________ _____ __ .__ ________
-// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
-// / | \ / /_\ \| | \ __\ | \ / ____/
-// / | \/ | \ | /| | | Y \/ \
-// \_______ /\____|__ /____/ |__| |___| /\_______ \
-// \/ \/ \/ \/
-
// Source holds configuration for the OAuth2 login source.
type Source struct {
Provider string
@@ -24,13 +17,15 @@ type Source struct {
CustomURLMapping *CustomURLMapping
IconURL string
- Scopes []string
- RequiredClaimName string
- RequiredClaimValue string
- GroupClaimName string
- AdminGroup string
- RestrictedGroup string
- SkipLocalTwoFA bool `json:",omitempty"`
+ Scopes []string
+ RequiredClaimName string
+ RequiredClaimValue string
+ GroupClaimName string
+ AdminGroup string
+ GroupTeamMap string
+ GroupTeamMapRemoval bool
+ RestrictedGroup string
+ SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
new file mode 100644
index 000000000..20b609534
--- /dev/null
+++ b/services/auth/source/source_group_sync.go
@@ -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
+}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 0cede07f9..5625aa1e2 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -72,13 +72,15 @@ type AuthenticationForm struct {
Oauth2GroupClaimName string
Oauth2AdminGroup string
Oauth2RestrictedGroup string
+ Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
+ Oauth2GroupTeamMapRemoval bool
SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
SSPIDefaultLanguage string
- GroupTeamMap string
+ GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
}