* 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:
parent
26718a785a
commit
832ce406ae
14 changed files with 423 additions and 65 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
100
services/auth/source/ldap/source_group_sync.go
Normal file
100
services/auth/source/ldap/source_group_sync.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue