mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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