* 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
					
				| @ -260,7 +260,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { | ||||
| 	if c.IsSet("skip-local-2fa") { | ||||
| 		config.SkipLocalTwoFA = c.Bool("skip-local-2fa") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -11,6 +11,9 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/services/auth" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -97,7 +100,13 @@ func getLDAPServerHost() string { | ||||
| 	return host | ||||
| } | ||||
| 
 | ||||
| func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) { | ||||
| func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string, groupMapParams ...string) { | ||||
| 	groupTeamMapRemoval := "off" | ||||
| 	groupTeamMap := "" | ||||
| 	if len(groupMapParams) == 2 { | ||||
| 		groupTeamMapRemoval = groupMapParams[0] | ||||
| 		groupTeamMap = groupMapParams[1] | ||||
| 	} | ||||
| 	session := loginUser(t, "user1") | ||||
| 	csrf := GetCSRF(t, session, "/admin/auths/new") | ||||
| 	req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{ | ||||
| @ -119,6 +128,12 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) { | ||||
| 		"attribute_ssh_public_key": sshKeyAttribute, | ||||
| 		"is_sync_enabled":          "on", | ||||
| 		"is_active":                "on", | ||||
| 		"groups_enabled":           "on", | ||||
| 		"group_dn":                 "ou=people,dc=planetexpress,dc=com", | ||||
| 		"group_member_uid":         "member", | ||||
| 		"group_team_map":           groupTeamMap, | ||||
| 		"group_team_map_removal":   groupTeamMapRemoval, | ||||
| 		"user_uid":                 "DN", | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusFound) | ||||
| } | ||||
| @ -294,3 +309,105 @@ func TestLDAPUserSSHKeySync(t *testing.T) { | ||||
| 		assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestLDAPGroupTeamSyncAddMember(t *testing.T) { | ||||
| 	if skipLDAPTests() { | ||||
| 		t.Skip() | ||||
| 		return | ||||
| 	} | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	addAuthSourceLDAP(t, "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) | ||||
| 	org, err := models.GetOrgByName("org26") | ||||
| 	assert.NoError(t, err) | ||||
| 	team, err := models.GetTeam(org.ID, "team11") | ||||
| 	assert.NoError(t, err) | ||||
| 	auth.SyncExternalUsers(context.Background(), true) | ||||
| 	for _, gitLDAPUser := range gitLDAPUsers { | ||||
| 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ | ||||
| 			Name: gitLDAPUser.UserName, | ||||
| 		}).(*user_model.User) | ||||
| 		usersOrgs, err := models.FindOrgs(models.FindOrgOptions{ | ||||
| 			UserID:         user.ID, | ||||
| 			IncludePrivate: true, | ||||
| 		}) | ||||
| 		assert.NoError(t, err) | ||||
| 		allOrgTeams, err := models.GetUserOrgTeams(org.ID, user.ID) | ||||
| 		assert.NoError(t, err) | ||||
| 		if user.Name == "fry" || user.Name == "leela" || user.Name == "bender" { | ||||
| 			// assert members of LDAP group "cn=ship_crew" are added to mapped teams
 | ||||
| 			assert.Equal(t, len(usersOrgs), 1, "User [%s] should be member of one organization", user.Name) | ||||
| 			assert.Equal(t, usersOrgs[0].Name, "org26", "Membership should be added to the right organization") | ||||
| 			isMember, err := models.IsTeamMember(usersOrgs[0].ID, team.ID, user.ID) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.True(t, isMember, "Membership should be added to the right team") | ||||
| 			err = team.RemoveMember(user.ID) | ||||
| 			assert.NoError(t, err) | ||||
| 			err = usersOrgs[0].RemoveMember(user.ID) | ||||
| 			assert.NoError(t, err) | ||||
| 		} else { | ||||
| 			// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
 | ||||
| 			assert.Empty(t, usersOrgs, "User should be member of no organization") | ||||
| 			isMember, err := models.IsTeamMember(org.ID, team.ID, user.ID) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.False(t, isMember, "User should no be added to this team") | ||||
| 			assert.Empty(t, allOrgTeams, "User should not be added to any team") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { | ||||
| 	if skipLDAPTests() { | ||||
| 		t.Skip() | ||||
| 		return | ||||
| 	} | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	addAuthSourceLDAP(t, "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) | ||||
| 	org, err := models.GetOrgByName("org26") | ||||
| 	assert.NoError(t, err) | ||||
| 	team, err := models.GetTeam(org.ID, "team11") | ||||
| 	assert.NoError(t, err) | ||||
| 	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password) | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ | ||||
| 		Name: gitLDAPUsers[0].UserName, | ||||
| 	}).(*user_model.User) | ||||
| 	err = org.AddMember(user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	err = team.AddMember(user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	isMember, err := models.IsOrganizationMember(org.ID, user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, isMember, "User should be member of this organization") | ||||
| 	isMember, err = models.IsTeamMember(org.ID, team.ID, user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, isMember, "User should be member of this team") | ||||
| 	// assert team member "professor" gets removed from org26 team11
 | ||||
| 	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password) | ||||
| 	isMember, err = models.IsOrganizationMember(org.ID, user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.False(t, isMember, "User membership should have been removed from organization") | ||||
| 	isMember, err = models.IsTeamMember(org.ID, team.ID, user.ID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.False(t, isMember, "User membership should have been removed from team") | ||||
| } | ||||
| 
 | ||||
| // Login should work even if Team Group Map contains a broken JSON
 | ||||
| func TestBrokenLDAPMapUserSignin(t *testing.T) { | ||||
| 	if skipLDAPTests() { | ||||
| 		t.Skip() | ||||
| 		return | ||||
| 	} | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	addAuthSourceLDAP(t, "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`) | ||||
| 
 | ||||
| 	u := gitLDAPUsers[0] | ||||
| 
 | ||||
| 	session := loginUserWithPassword(t, u.UserName, u.Password) | ||||
| 	req := NewRequest(t, "GET", "/user/settings") | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 	htmlDoc := NewHTMLParser(t, resp.Body) | ||||
| 
 | ||||
| 	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) | ||||
| 	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) | ||||
| 	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text()) | ||||
| } | ||||
|  | ||||
| @ -2581,11 +2581,13 @@ auths.filter = User Filter | ||||
| auths.admin_filter = Admin Filter | ||||
| auths.restricted_filter = Restricted Filter | ||||
| auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted. | ||||
| auths.verify_group_membership = Verify group membership in LDAP | ||||
| auths.verify_group_membership = Verify group membership in LDAP (leave the filter empty to skip) | ||||
| auths.group_search_base = Group Search Base DN | ||||
| auths.valid_groups_filter = Valid Groups Filter | ||||
| auths.group_attribute_list_users = Group Attribute Containing List Of Users | ||||
| auths.user_attribute_in_group = User Attribute Listed In Group | ||||
| auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip) | ||||
| auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group | ||||
| auths.enable_ldap_groups = Enable LDAP groups | ||||
| auths.ms_ad_sa = MS AD Search Attributes | ||||
| auths.smtp_auth = SMTP Authentication Type | ||||
| auths.smtphost = SMTP Host | ||||
|  | ||||
| @ -145,6 +145,8 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source { | ||||
| 		GroupDN:               form.GroupDN, | ||||
| 		GroupFilter:           form.GroupFilter, | ||||
| 		GroupMemberUID:        form.GroupMemberUID, | ||||
| 		GroupTeamMap:          form.GroupTeamMap, | ||||
| 		GroupTeamMapRemoval:   form.GroupTeamMapRemoval, | ||||
| 		UserUID:               form.UserUID, | ||||
| 		AdminFilter:           form.AdminFilter, | ||||
| 		RestrictedFilter:      form.RestrictedFilter, | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -79,6 +79,8 @@ type AuthenticationForm struct { | ||||
| 	SSPIStripDomainNames          bool | ||||
| 	SSPISeparatorReplacement      string `binding:"AlphaDashDot;MaxSize(5)"` | ||||
| 	SSPIDefaultLanguage           string | ||||
| 	GroupTeamMap                  string | ||||
| 	GroupTeamMapRemoval           bool | ||||
| } | ||||
| 
 | ||||
| // Validate validates fields
 | ||||
|  | ||||
| @ -108,31 +108,43 @@ | ||||
| 						<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label> | ||||
| 						<input id="attribute_avatar" name="attribute_avatar" value="{{$cfg.AttributeAvatar}}" placeholder="e.g. jpegPhoto"> | ||||
| 					</div> | ||||
| 
 | ||||
| 
 | ||||
| 					<!-- ldap group begin --> | ||||
| 					<div class="inline field"> | ||||
| 						<div class="ui checkbox"> | ||||
| 							<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label> | ||||
| 							<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}> | ||||
| 							<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label> | ||||
| 							<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div id="groups_enabled_change"> | ||||
| 					<div id="ldap-group-options" class="ui segment secondary" {{if not $cfg.GroupsEnabled}}hidden{{end}}> | ||||
| 						<div class="field"> | ||||
| 							<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label> | ||||
| 							<input id="group_dn" name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.group_search_base"}}</label> | ||||
| 							<input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label> | ||||
| 							<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||
| 							<input name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid"> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||
| 							<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||
| 							<input name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid"> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||
| 							<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label> | ||||
| 							<input name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | ||||
| 						</div> | ||||
| 						<div class="field"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.map_group_to_team"}}</label> | ||||
| 							<input name="group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'> | ||||
| 						</div> | ||||
| 						<div class="ui checkbox"> | ||||
| 							<label>{{.i18n.Tr "admin.auths.map_group_to_team_removal"}}</label> | ||||
| 							<input name="group_team_map_removal" type="checkbox" {{if $cfg.GroupTeamMapRemoval}}checked{{end}}> | ||||
| 						</div> | ||||
| 						<br/> | ||||
| 					</div> | ||||
| 					<!-- ldap group end --> | ||||
| 
 | ||||
| 					{{if .Source.IsLDAP}} | ||||
| 						<div class="inline field"> | ||||
| 							<div class="ui checkbox"> | ||||
|  | ||||
| @ -79,31 +79,42 @@ | ||||
| 		<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label> | ||||
| 		<input id="attribute_avatar" name="attribute_avatar" value="{{.attribute_avatar}}" placeholder="e.g. jpegPhoto"> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<!-- ldap group begin --> | ||||
| 	<div class="inline field"> | ||||
| 		<div class="ui checkbox"> | ||||
| 			<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label> | ||||
| 			<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}> | ||||
| 			<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label> | ||||
| 			<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if .groups_enabled}}checked{{end}}> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div id="groups_enabled_change"> | ||||
| 	<div id="ldap-group-options" class="ui segment secondary"> | ||||
| 		<div class="field"> | ||||
| 			<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label> | ||||
| 			<input id="group_dn" name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.group_search_base"}}</label> | ||||
| 			<input name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||
| 		</div> | ||||
| 		<div class="field"> | ||||
| 			<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label> | ||||
| 			<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||
| 			<input name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid"> | ||||
| 		</div> | ||||
| 		<div class="field"> | ||||
| 			<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||
| 			<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||
| 			<input name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid"> | ||||
| 		</div> | ||||
| 		<div class="field"> | ||||
| 			<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||
| 			<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label> | ||||
| 			<input name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | ||||
| 		</div> | ||||
| 		<div class="field"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.map_group_to_team"}}</label> | ||||
| 			<input name="group_team_map" value="{{.group_team_map}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'> | ||||
| 		</div> | ||||
| 		<div class="ui checkbox"> | ||||
| 			<label>{{.i18n.Tr "admin.auths.map_group_to_team_removal"}}</label> | ||||
| 			<input name="group_team_map_removal" type="checkbox" {{if .group_team_map_removal}}checked{{end}}> | ||||
| 		</div> | ||||
| 		<br/> | ||||
| 	</div> | ||||
| 	<!-- ldap group end --> | ||||
| 
 | ||||
| 	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}"> | ||||
| 		<div class="ui checkbox"> | ||||
| 			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label> | ||||
|  | ||||
| @ -91,12 +91,8 @@ export function initAdminCommon() { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function onVerifyGroupMembershipChange() { | ||||
|     if ($('#groups_enabled').is(':checked')) { | ||||
|       $('#groups_enabled_change').show(); | ||||
|     } else { | ||||
|       $('#groups_enabled_change').hide(); | ||||
|     } | ||||
|   function onEnableLdapGroupsChange() { | ||||
|     $('#ldap-group-options').toggle($('.js-ldap-group-toggle').is(':checked')); | ||||
|   } | ||||
| 
 | ||||
|   // New authentication
 | ||||
| @ -139,7 +135,7 @@ export function initAdminCommon() { | ||||
|       } | ||||
|       if (authType === '2' || authType === '5') { | ||||
|         onSecurityProtocolChange(); | ||||
|         onVerifyGroupMembershipChange(); | ||||
|         onEnableLdapGroupsChange(); | ||||
|       } | ||||
|       if (authType === '2') { | ||||
|         onUsePagedSearchChange(); | ||||
| @ -150,15 +146,15 @@ export function initAdminCommon() { | ||||
|     $('#use_paged_search').on('change', onUsePagedSearchChange); | ||||
|     $('#oauth2_provider').on('change', () => onOAuth2Change(true)); | ||||
|     $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true)); | ||||
|     $('#groups_enabled').on('change', onVerifyGroupMembershipChange); | ||||
|     $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); | ||||
|   } | ||||
|   // Edit authentication
 | ||||
|   if ($('.admin.edit.authentication').length > 0) { | ||||
|     const authType = $('#auth_type').val(); | ||||
|     if (authType === '2' || authType === '5') { | ||||
|       $('#security_protocol').on('change', onSecurityProtocolChange); | ||||
|       $('#groups_enabled').on('change', onVerifyGroupMembershipChange); | ||||
|       onVerifyGroupMembershipChange(); | ||||
|       $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); | ||||
|       onEnableLdapGroupsChange(); | ||||
|       if (authType === '2') { | ||||
|         $('#use_paged_search').on('change', onUsePagedSearchChange); | ||||
|       } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user