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.
This commit is contained in:
Sven Seeberg 2022-02-11 15:24:58 +01:00 committed by GitHub
parent 26718a785a
commit 832ce406ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 423 additions and 65 deletions

View file

@ -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)