summaryrefslogtreecommitdiffstats
path: root/services/auth
diff options
context:
space:
mode:
authorSven Seeberg <sven@geeq.de>2022-02-11 15:24:58 +0100
committerGitHub <noreply@github.com>2022-02-11 22:24:58 +0800
commit832ce406aefed0cceb30d42d1435f425a9aba279 (patch)
tree7089998c621efe706fba829be9d6c31d97210ee7 /services/auth
parent26718a785ac49f17eab51ad0f5324d036b810f73 (diff)
downloadgitea-832ce406aefed0cceb30d42d1435f425a9aba279.tar.gz
gitea-832ce406aefed0cceb30d42d1435f425a9aba279.zip
Add LDAP group sync to Teams, fixes #1395 (#16299)
* Add setting for a JSON that maps LDAP groups to Org Teams. * Add log when removing or adding team members. * Sync is being run on login and periodically. * Existing group filter settings are reused. * Adding and removing team members. * Sync not existing LDAP group. * Login with broken group map JSON.
Diffstat (limited to 'services/auth')
-rw-r--r--services/auth/source/ldap/README.md8
-rw-r--r--services/auth/source/ldap/source.go2
-rw-r--r--services/auth/source/ldap/source_authenticate.go13
-rw-r--r--services/auth/source/ldap/source_group_sync.go100
-rw-r--r--services/auth/source/ldap/source_search.go141
-rw-r--r--services/auth/source/ldap/source_sync.go7
6 files changed, 244 insertions, 27 deletions
diff --git a/services/auth/source/ldap/README.md b/services/auth/source/ldap/README.md
index 3a839fa314..59fc5cabad 100644
--- a/services/auth/source/ldap/README.md
+++ b/services/auth/source/ldap/README.md
@@ -120,3 +120,11 @@ share the following fields:
* Group Attribute for User (optional)
* Which group LDAP attribute contains an array above user attribute names.
* Example: memberUid
+
+* Team group map (optional)
+ * Automatically add users to Organization teams, depending on LDAP group memberships.
+ * Note: this function only adds users to teams, it never removes users.
+ * Example: {"cn=MyGroup,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2", ...], ...}, ...}
+
+* Team group map removal (optional)
+ * If set to true, users will be removed from teams if they are not members of the corresponding group.
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index fc778b0114..ad97e2dd49 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -52,6 +52,8 @@ type Source struct {
GroupDN string // Group Search Base
GroupFilter string // Group Name Filter
GroupMemberUID string // Group Attribute containing array of UserUID
+ GroupTeamMap string // Map LDAP groups to teams
+ GroupTeamMapRemoval bool // Remove user from teams which are synchronized and user is not a member of the corresponding LDAP group
UserUID string // User Attribute listed in Group
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 52971bb87e..e804e32e84 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -8,6 +8,7 @@ import (
"fmt"
"strings"
+ "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@@ -59,10 +60,14 @@ 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]*models.Organization)
+ teamCache := make(map[string]*models.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()
}
-
return user, nil
}
@@ -98,10 +103,14 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
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 source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+ orgCache := make(map[string]*models.Organization)
+ teamCache := make(map[string]*models.Team)
+ source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
+ }
return user, err
}
diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go
new file mode 100644
index 0000000000..7c62af705e
--- /dev/null
+++ b/services/auth/source/ldap/source_group_sync.go
@@ -0,0 +1,100 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package ldap
+
+import (
+ "code.gitea.io/gitea/models"
+ 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]*models.Organization, teamCache map[string]*models.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 = models.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
+ }
+ if isMember, err := models.IsOrganizationMember(org.ID, user.ID); !isMember && err == nil {
+ log.Trace("LDAP group sync: adding user [%s] to organization [%s]", user.Name, org.Name)
+ err = org.AddMember(user.ID)
+ if err != nil {
+ log.Error("LDAP group sync: Could not add user to organization: %v", err)
+ continue
+ }
+ }
+ 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 := models.IsTeamMember(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 := team.AddMember(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]*models.Organization, teamCache map[string]*models.Team) {
+ var err error
+ for orgName, teamNames := range ldapTeamRemove {
+ org, ok := orgCache[orgName]
+ if !ok {
+ org, err = models.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 := models.IsTeamMember(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 = team.RemoveMember(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 1f1cca270d..f2b940cabe 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -12,22 +12,26 @@ import (
"strconv"
"strings"
+ "code.gitea.io/gitea/modules/json"
"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
+ 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
}
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -192,6 +196,71 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
return false
}
+// List all group memberships of a user
+func (ls *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
+ var ldapGroups []string
+ groupFilter := fmt.Sprintf("(%s=%s)", ls.GroupMemberUID, uid)
+ result, err := l.Search(ldap.NewSearchRequest(
+ ls.GroupDN,
+ ldap.ScopeWholeSubtree,
+ ldap.NeverDerefAliases,
+ 0,
+ 0,
+ false,
+ groupFilter,
+ []string{},
+ nil,
+ ))
+ if err != nil {
+ log.Error("Failed group search using filter[%s]: %v", groupFilter, err)
+ return ldapGroups
+ }
+
+ for _, entry := range result.Entries {
+ if entry.DN == "" {
+ log.Error("LDAP search was successful, but found no DN!")
+ continue
+ }
+ ldapGroups = append(ldapGroups, entry.DN)
+ }
+
+ return ldapGroups
+}
+
+// parse LDAP groups and return map of ldap groups to organizations teams
+func (ls *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
+ ldapGroupsToTeams := make(map[string]map[string][]string)
+ err := json.Unmarshal([]byte(ls.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 (ls *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
+ // get all LDAP group memberships for user
+ usersLdapGroups := ls.listLdapGroupMemberships(l, uid)
+ // unmarshall LDAP group team map from configs
+ ldapGroupsToTeams := ls.mapLdapGroupsToTeams()
+ membershipsToAdd := map[string][]string{}
+ membershipsToRemove := map[string][]string{}
+ for group, memberships := range ldapGroupsToTeams {
+ isUserInGroup := util.IsStringInSlice(group, usersLdapGroups)
+ if isUserInGroup {
+ for org, teams := range memberships {
+ membershipsToAdd[org] = teams
+ }
+ } else if !isUserInGroup {
+ for org, teams := range memberships {
+ membershipsToRemove[org] = teams
+ }
+ }
+ }
+ return membershipsToAdd, membershipsToRemove
+}
+
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
@@ -308,9 +377,12 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
+ if ls.UserUID == "dn" || ls.UserUID == "DN" {
+ uid = sr.Entries[0].DN
+ }
// Check group membership
- if ls.GroupsEnabled {
+ if ls.GroupsEnabled && ls.GroupFilter != "" {
groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
if !ok {
return nil
@@ -373,16 +445,24 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar)
}
+ teamsToAdd := make(map[string][]string)
+ teamsToRemove := make(map[string][]string)
+ if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
+ teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, uid)
+ }
+
return &SearchResult{
- LowerName: strings.ToLower(username),
- Username: username,
- Name: firstname,
- Surname: surname,
- Mail: mail,
- SSHPublicKey: sshPublicKey,
- IsAdmin: isAdmin,
- IsRestricted: isRestricted,
- Avatar: Avatar,
+ LowerName: strings.ToLower(username),
+ Username: username,
+ Name: firstname,
+ Surname: surname,
+ Mail: mail,
+ SSHPublicKey: sshPublicKey,
+ IsAdmin: isAdmin,
+ IsRestricted: isRestricted,
+ Avatar: Avatar,
+ LdapTeamAdd: teamsToAdd,
+ LdapTeamRemove: teamsToRemove,
}
}
@@ -417,7 +497,7 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0
- attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
+ attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID}
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}
@@ -444,12 +524,23 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
result := make([]*SearchResult, len(sr.Entries))
for i, v := range sr.Entries {
+ teamsToAdd := make(map[string][]string)
+ teamsToRemove := make(map[string][]string)
+ if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
+ userAttributeListedInGroup := v.GetAttributeValue(ls.UserUID)
+ if ls.UserUID == "dn" || ls.UserUID == "DN" {
+ userAttributeListedInGroup = v.DN
+ }
+ teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, userAttributeListedInGroup)
+ }
result[i] = &SearchResult{
- Username: v.GetAttributeValue(ls.AttributeUsername),
- Name: v.GetAttributeValue(ls.AttributeName),
- Surname: v.GetAttributeValue(ls.AttributeSurname),
- Mail: v.GetAttributeValue(ls.AttributeMail),
- IsAdmin: checkAdmin(l, ls, v.DN),
+ Username: v.GetAttributeValue(ls.AttributeUsername),
+ Name: v.GetAttributeValue(ls.AttributeName),
+ Surname: v.GetAttributeValue(ls.AttributeSurname),
+ Mail: v.GetAttributeValue(ls.AttributeMail),
+ IsAdmin: checkAdmin(l, ls, v.DN),
+ LdapTeamAdd: teamsToAdd,
+ LdapTeamRemove: teamsToRemove,
}
if !result[i].IsAdmin {
result[i].IsRestricted = checkRestricted(l, ls, v.DN)
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 398d9ef798..74a62ce4e7 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -10,6 +10,7 @@ import (
"sort"
"strings"
+ "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
@@ -61,6 +62,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
})
userPos := 0
+ orgCache := make(map[string]*models.Organization)
+ teamCache := make(map[string]*models.Team)
for _, su := range sr {
select {
@@ -166,6 +169,10 @@ 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)
+ }
}
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed