mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	* 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:
		| @@ -260,7 +260,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { | |||||||
| 	if c.IsSet("skip-local-2fa") { | 	if c.IsSet("skip-local-2fa") { | ||||||
| 		config.SkipLocalTwoFA = c.Bool("skip-local-2fa") | 		config.SkipLocalTwoFA = c.Bool("skip-local-2fa") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,9 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"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" | 	"code.gitea.io/gitea/services/auth" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -97,7 +100,13 @@ func getLDAPServerHost() string { | |||||||
| 	return host | 	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") | 	session := loginUser(t, "user1") | ||||||
| 	csrf := GetCSRF(t, session, "/admin/auths/new") | 	csrf := GetCSRF(t, session, "/admin/auths/new") | ||||||
| 	req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{ | 	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, | 		"attribute_ssh_public_key": sshKeyAttribute, | ||||||
| 		"is_sync_enabled":          "on", | 		"is_sync_enabled":          "on", | ||||||
| 		"is_active":                "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) | 	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) | 		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.admin_filter = Admin Filter | ||||||
| auths.restricted_filter = Restricted 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.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.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.group_attribute_list_users = Group Attribute Containing List Of Users | ||||||
| auths.user_attribute_in_group = User Attribute Listed In Group | 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.ms_ad_sa = MS AD Search Attributes | ||||||
| auths.smtp_auth = SMTP Authentication Type | auths.smtp_auth = SMTP Authentication Type | ||||||
| auths.smtphost = SMTP Host | auths.smtphost = SMTP Host | ||||||
|   | |||||||
| @@ -145,6 +145,8 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source { | |||||||
| 		GroupDN:               form.GroupDN, | 		GroupDN:               form.GroupDN, | ||||||
| 		GroupFilter:           form.GroupFilter, | 		GroupFilter:           form.GroupFilter, | ||||||
| 		GroupMemberUID:        form.GroupMemberUID, | 		GroupMemberUID:        form.GroupMemberUID, | ||||||
|  | 		GroupTeamMap:          form.GroupTeamMap, | ||||||
|  | 		GroupTeamMapRemoval:   form.GroupTeamMapRemoval, | ||||||
| 		UserUID:               form.UserUID, | 		UserUID:               form.UserUID, | ||||||
| 		AdminFilter:           form.AdminFilter, | 		AdminFilter:           form.AdminFilter, | ||||||
| 		RestrictedFilter:      form.RestrictedFilter, | 		RestrictedFilter:      form.RestrictedFilter, | ||||||
|   | |||||||
| @@ -120,3 +120,11 @@ share the following fields: | |||||||
| * Group Attribute for User (optional) | * Group Attribute for User (optional) | ||||||
|   * Which group LDAP attribute contains an array above user attribute names. |   * Which group LDAP attribute contains an array above user attribute names. | ||||||
|   * Example: memberUid |   * 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 | 	GroupDN               string // Group Search Base | ||||||
| 	GroupFilter           string // Group Name Filter | 	GroupFilter           string // Group Name Filter | ||||||
| 	GroupMemberUID        string // Group Attribute containing array of UserUID | 	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 | 	UserUID               string // User Attribute listed in Group | ||||||
| 	SkipLocalTwoFA        bool   `json:",omitempty"` // Skip Local 2fa for users authenticated with this source | 	SkipLocalTwoFA        bool   `json:",omitempty"` // Skip Local 2fa for users authenticated with this source | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/auth" | 	"code.gitea.io/gitea/models/auth" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"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 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) { | 		if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) { | ||||||
| 			return user, asymkey_model.RewriteAllPublicKeys() | 			return user, asymkey_model.RewriteAllPublicKeys() | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return user, nil | 		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) { | 	if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) { | ||||||
| 		err = asymkey_model.RewriteAllPublicKeys() | 		err = asymkey_model.RewriteAllPublicKeys() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err == nil && len(source.AttributeAvatar) > 0 { | 	if err == nil && len(source.AttributeAvatar) > 0 { | ||||||
| 		_ = user_service.UploadAvatar(user, sr.Avatar) | 		_ = 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 | 	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" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/json" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  |  | ||||||
| 	"github.com/go-ldap/ldap/v3" | 	"github.com/go-ldap/ldap/v3" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // SearchResult : user data | // SearchResult : user data | ||||||
| type SearchResult struct { | type SearchResult struct { | ||||||
| 	Username     string   // Username | 	Username       string   // Username | ||||||
| 	Name         string   // Name | 	Name           string   // Name | ||||||
| 	Surname      string   // Surname | 	Surname        string   // Surname | ||||||
| 	Mail         string   // E-mail address | 	Mail           string   // E-mail address | ||||||
| 	SSHPublicKey []string // SSH Public Key | 	SSHPublicKey   []string // SSH Public Key | ||||||
| 	IsAdmin      bool     // if user is administrator | 	IsAdmin        bool     // if user is administrator | ||||||
| 	IsRestricted bool     // if user is restricted | 	IsRestricted   bool     // if user is restricted | ||||||
| 	LowerName    string   // Lowername | 	LowerName      string   // LowerName | ||||||
| 	Avatar       []byte | 	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) { | func (ls *Source) sanitizedUserQuery(username string) (string, bool) { | ||||||
| @@ -192,6 +196,71 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { | |||||||
| 	return false | 	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 | // 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 { | func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult { | ||||||
| 	// See https://tools.ietf.org/search/rfc4513#section-5.1.2 | 	// 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) | 	surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) | ||||||
| 	mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) | 	mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) | ||||||
| 	uid := sr.Entries[0].GetAttributeValue(ls.UserUID) | 	uid := sr.Entries[0].GetAttributeValue(ls.UserUID) | ||||||
|  | 	if ls.UserUID == "dn" || ls.UserUID == "DN" { | ||||||
|  | 		uid = sr.Entries[0].DN | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Check group membership | 	// Check group membership | ||||||
| 	if ls.GroupsEnabled { | 	if ls.GroupsEnabled && ls.GroupFilter != "" { | ||||||
| 		groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter) | 		groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return nil | 			return nil | ||||||
| @@ -373,16 +445,24 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul | |||||||
| 		Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar) | 		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{ | 	return &SearchResult{ | ||||||
| 		LowerName:    strings.ToLower(username), | 		LowerName:      strings.ToLower(username), | ||||||
| 		Username:     username, | 		Username:       username, | ||||||
| 		Name:         firstname, | 		Name:           firstname, | ||||||
| 		Surname:      surname, | 		Surname:        surname, | ||||||
| 		Mail:         mail, | 		Mail:           mail, | ||||||
| 		SSHPublicKey: sshPublicKey, | 		SSHPublicKey:   sshPublicKey, | ||||||
| 		IsAdmin:      isAdmin, | 		IsAdmin:        isAdmin, | ||||||
| 		IsRestricted: isRestricted, | 		IsRestricted:   isRestricted, | ||||||
| 		Avatar:       Avatar, | 		Avatar:         Avatar, | ||||||
|  | 		LdapTeamAdd:    teamsToAdd, | ||||||
|  | 		LdapTeamRemove: teamsToRemove, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -417,7 +497,7 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) { | |||||||
| 	isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 | 	isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 | ||||||
| 	isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 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 { | 	if isAttributeSSHPublicKeySet { | ||||||
| 		attribs = append(attribs, ls.AttributeSSHPublicKey) | 		attribs = append(attribs, ls.AttributeSSHPublicKey) | ||||||
| 	} | 	} | ||||||
| @@ -444,12 +524,23 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) { | |||||||
| 	result := make([]*SearchResult, len(sr.Entries)) | 	result := make([]*SearchResult, len(sr.Entries)) | ||||||
|  |  | ||||||
| 	for i, v := range 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{ | 		result[i] = &SearchResult{ | ||||||
| 			Username: v.GetAttributeValue(ls.AttributeUsername), | 			Username:       v.GetAttributeValue(ls.AttributeUsername), | ||||||
| 			Name:     v.GetAttributeValue(ls.AttributeName), | 			Name:           v.GetAttributeValue(ls.AttributeName), | ||||||
| 			Surname:  v.GetAttributeValue(ls.AttributeSurname), | 			Surname:        v.GetAttributeValue(ls.AttributeSurname), | ||||||
| 			Mail:     v.GetAttributeValue(ls.AttributeMail), | 			Mail:           v.GetAttributeValue(ls.AttributeMail), | ||||||
| 			IsAdmin:  checkAdmin(l, ls, v.DN), | 			IsAdmin:        checkAdmin(l, ls, v.DN), | ||||||
|  | 			LdapTeamAdd:    teamsToAdd, | ||||||
|  | 			LdapTeamRemove: teamsToRemove, | ||||||
| 		} | 		} | ||||||
| 		if !result[i].IsAdmin { | 		if !result[i].IsAdmin { | ||||||
| 			result[i].IsRestricted = checkRestricted(l, ls, v.DN) | 			result[i].IsRestricted = checkRestricted(l, ls, v.DN) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -61,6 +62,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	userPos := 0 | 	userPos := 0 | ||||||
|  | 	orgCache := make(map[string]*models.Organization) | ||||||
|  | 	teamCache := make(map[string]*models.Team) | ||||||
|  |  | ||||||
| 	for _, su := range sr { | 	for _, su := range sr { | ||||||
| 		select { | 		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 | 	// 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 | 	SSPIStripDomainNames          bool | ||||||
| 	SSPISeparatorReplacement      string `binding:"AlphaDashDot;MaxSize(5)"` | 	SSPISeparatorReplacement      string `binding:"AlphaDashDot;MaxSize(5)"` | ||||||
| 	SSPIDefaultLanguage           string | 	SSPIDefaultLanguage           string | ||||||
|  | 	GroupTeamMap                  string | ||||||
|  | 	GroupTeamMapRemoval           bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // Validate validates fields | // Validate validates fields | ||||||
|   | |||||||
| @@ -108,31 +108,43 @@ | |||||||
| 						<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label> | 						<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"> | 						<input id="attribute_avatar" name="attribute_avatar" value="{{$cfg.AttributeAvatar}}" placeholder="e.g. jpegPhoto"> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 					<!-- ldap group begin --> | ||||||
| 					<div class="inline field"> | 					<div class="inline field"> | ||||||
| 						<div class="ui checkbox"> | 						<div class="ui checkbox"> | ||||||
| 							<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label> | 							<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label> | ||||||
| 							<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}> | 							<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}> | ||||||
| 						</div> | 						</div> | ||||||
| 					</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"> | 						<div class="field"> | ||||||
| 							<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label> | 							<label>{{.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"> | 							<input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class="field"> | 						<div class="field"> | ||||||
| 							<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label> | 							<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||||
| 							<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | 							<input name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid"> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class="field"> | 						<div class="field"> | ||||||
| 							<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | 							<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||||
| 							<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid"> | 							<input name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid"> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class="field"> | 						<div class="field"> | ||||||
| 							<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | 							<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label> | ||||||
| 							<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid"> | 							<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> | 						</div> | ||||||
| 						<br/> |  | ||||||
| 					</div> | 					</div> | ||||||
|  | 					<!-- ldap group end --> | ||||||
|  |  | ||||||
| 					{{if .Source.IsLDAP}} | 					{{if .Source.IsLDAP}} | ||||||
| 						<div class="inline field"> | 						<div class="inline field"> | ||||||
| 							<div class="ui checkbox"> | 							<div class="ui checkbox"> | ||||||
|   | |||||||
| @@ -79,31 +79,42 @@ | |||||||
| 		<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label> | 		<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"> | 		<input id="attribute_avatar" name="attribute_avatar" value="{{.attribute_avatar}}" placeholder="e.g. jpegPhoto"> | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
|  | 	<!-- ldap group begin --> | ||||||
| 	<div class="inline field"> | 	<div class="inline field"> | ||||||
| 		<div class="ui checkbox"> | 		<div class="ui checkbox"> | ||||||
| 			<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label> | 			<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label> | ||||||
| 			<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}> | 			<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if .groups_enabled}}checked{{end}}> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div id="groups_enabled_change"> | 	<div id="ldap-group-options" class="ui segment secondary"> | ||||||
| 		<div class="field"> | 		<div class="field"> | ||||||
| 			<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label> | 			<label>{{.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"> | 			<input name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com"> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="field"> | 		<div class="field"> | ||||||
| 			<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label> | 			<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | ||||||
| 			<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))"> | 			<input name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid"> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="field"> | 		<div class="field"> | ||||||
| 			<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label> | 			<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | ||||||
| 			<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid"> | 			<input name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid"> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="field"> | 		<div class="field"> | ||||||
| 			<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label> | 			<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label> | ||||||
| 			<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid"> | 			<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> | 		</div> | ||||||
| 		<br/> |  | ||||||
| 	</div> | 	</div> | ||||||
|  | 	<!-- ldap group end --> | ||||||
|  |  | ||||||
| 	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}"> | 	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}"> | ||||||
| 		<div class="ui checkbox"> | 		<div class="ui checkbox"> | ||||||
| 			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label> | 			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label> | ||||||
|   | |||||||
| @@ -91,12 +91,8 @@ export function initAdminCommon() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   function onVerifyGroupMembershipChange() { |   function onEnableLdapGroupsChange() { | ||||||
|     if ($('#groups_enabled').is(':checked')) { |     $('#ldap-group-options').toggle($('.js-ldap-group-toggle').is(':checked')); | ||||||
|       $('#groups_enabled_change').show(); |  | ||||||
|     } else { |  | ||||||
|       $('#groups_enabled_change').hide(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // New authentication |   // New authentication | ||||||
| @@ -139,7 +135,7 @@ export function initAdminCommon() { | |||||||
|       } |       } | ||||||
|       if (authType === '2' || authType === '5') { |       if (authType === '2' || authType === '5') { | ||||||
|         onSecurityProtocolChange(); |         onSecurityProtocolChange(); | ||||||
|         onVerifyGroupMembershipChange(); |         onEnableLdapGroupsChange(); | ||||||
|       } |       } | ||||||
|       if (authType === '2') { |       if (authType === '2') { | ||||||
|         onUsePagedSearchChange(); |         onUsePagedSearchChange(); | ||||||
| @@ -150,15 +146,15 @@ export function initAdminCommon() { | |||||||
|     $('#use_paged_search').on('change', onUsePagedSearchChange); |     $('#use_paged_search').on('change', onUsePagedSearchChange); | ||||||
|     $('#oauth2_provider').on('change', () => onOAuth2Change(true)); |     $('#oauth2_provider').on('change', () => onOAuth2Change(true)); | ||||||
|     $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true)); |     $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true)); | ||||||
|     $('#groups_enabled').on('change', onVerifyGroupMembershipChange); |     $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); | ||||||
|   } |   } | ||||||
|   // Edit authentication |   // Edit authentication | ||||||
|   if ($('.admin.edit.authentication').length > 0) { |   if ($('.admin.edit.authentication').length > 0) { | ||||||
|     const authType = $('#auth_type').val(); |     const authType = $('#auth_type').val(); | ||||||
|     if (authType === '2' || authType === '5') { |     if (authType === '2' || authType === '5') { | ||||||
|       $('#security_protocol').on('change', onSecurityProtocolChange); |       $('#security_protocol').on('change', onSecurityProtocolChange); | ||||||
|       $('#groups_enabled').on('change', onVerifyGroupMembershipChange); |       $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); | ||||||
|       onVerifyGroupMembershipChange(); |       onEnableLdapGroupsChange(); | ||||||
|       if (authType === '2') { |       if (authType === '2') { | ||||||
|         $('#use_paged_search').on('change', onUsePagedSearchChange); |         $('#use_paged_search').on('change', onUsePagedSearchChange); | ||||||
|       } |       } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user