code.gitea.io/gitea@v1.21.7/tests/integration/auth_ldap_test.go (about)

     1  // Copyright 2018 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"code.gitea.io/gitea/models"
    14  	auth_model "code.gitea.io/gitea/models/auth"
    15  	"code.gitea.io/gitea/models/db"
    16  	"code.gitea.io/gitea/models/organization"
    17  	"code.gitea.io/gitea/models/unittest"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/translation"
    20  	"code.gitea.io/gitea/services/auth"
    21  	"code.gitea.io/gitea/services/auth/source/ldap"
    22  	"code.gitea.io/gitea/tests"
    23  
    24  	"github.com/stretchr/testify/assert"
    25  )
    26  
    27  type ldapUser struct {
    28  	UserName     string
    29  	Password     string
    30  	FullName     string
    31  	Email        string
    32  	OtherEmails  []string
    33  	IsAdmin      bool
    34  	IsRestricted bool
    35  	SSHKeys      []string
    36  }
    37  
    38  var gitLDAPUsers = []ldapUser{
    39  	{
    40  		UserName:    "professor",
    41  		Password:    "professor",
    42  		FullName:    "Hubert Farnsworth",
    43  		Email:       "professor@planetexpress.com",
    44  		OtherEmails: []string{"hubert@planetexpress.com"},
    45  		IsAdmin:     true,
    46  	},
    47  	{
    48  		UserName: "hermes",
    49  		Password: "hermes",
    50  		FullName: "Conrad Hermes",
    51  		Email:    "hermes@planetexpress.com",
    52  		SSHKeys: []string{
    53  			"SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8",
    54  			"SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ",
    55  			"SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI",
    56  		},
    57  		IsAdmin: true,
    58  	},
    59  	{
    60  		UserName: "fry",
    61  		Password: "fry",
    62  		FullName: "Philip Fry",
    63  		Email:    "fry@planetexpress.com",
    64  	},
    65  	{
    66  		UserName:     "leela",
    67  		Password:     "leela",
    68  		FullName:     "Leela Turanga",
    69  		Email:        "leela@planetexpress.com",
    70  		IsRestricted: true,
    71  	},
    72  	{
    73  		UserName: "bender",
    74  		Password: "bender",
    75  		FullName: "Bender Rodríguez",
    76  		Email:    "bender@planetexpress.com",
    77  	},
    78  }
    79  
    80  var otherLDAPUsers = []ldapUser{
    81  	{
    82  		UserName: "zoidberg",
    83  		Password: "zoidberg",
    84  		FullName: "John Zoidberg",
    85  		Email:    "zoidberg@planetexpress.com",
    86  	},
    87  	{
    88  		UserName: "amy",
    89  		Password: "amy",
    90  		FullName: "Amy Kroker",
    91  		Email:    "amy@planetexpress.com",
    92  	},
    93  }
    94  
    95  func skipLDAPTests() bool {
    96  	return os.Getenv("TEST_LDAP") != "1"
    97  }
    98  
    99  func getLDAPServerHost() string {
   100  	host := os.Getenv("TEST_LDAP_HOST")
   101  	if len(host) == 0 {
   102  		host = "ldap"
   103  	}
   104  	return host
   105  }
   106  
   107  func getLDAPServerPort() string {
   108  	port := os.Getenv("TEST_LDAP_PORT")
   109  	if len(port) == 0 {
   110  		port = "389"
   111  	}
   112  	return port
   113  }
   114  
   115  func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string {
   116  	// Modify user filter to test group filter explicitly
   117  	userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
   118  	if groupFilter != "" {
   119  		userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))"
   120  	}
   121  
   122  	return map[string]string{
   123  		"_csrf":                    csrf,
   124  		"type":                     "2",
   125  		"name":                     "ldap",
   126  		"host":                     getLDAPServerHost(),
   127  		"port":                     getLDAPServerPort(),
   128  		"bind_dn":                  "uid=gitea,ou=service,dc=planetexpress,dc=com",
   129  		"bind_password":            "password",
   130  		"user_base":                "ou=people,dc=planetexpress,dc=com",
   131  		"filter":                   userFilter,
   132  		"admin_filter":             "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
   133  		"restricted_filter":        "(uid=leela)",
   134  		"attribute_username":       "uid",
   135  		"attribute_name":           "givenName",
   136  		"attribute_surname":        "sn",
   137  		"attribute_mail":           "mail",
   138  		"attribute_ssh_public_key": sshKeyAttribute,
   139  		"is_sync_enabled":          "on",
   140  		"is_active":                "on",
   141  		"groups_enabled":           "on",
   142  		"group_dn":                 "ou=people,dc=planetexpress,dc=com",
   143  		"group_member_uid":         "member",
   144  		"group_filter":             groupFilter,
   145  		"group_team_map":           groupTeamMap,
   146  		"group_team_map_removal":   groupTeamMapRemoval,
   147  		"user_uid":                 "DN",
   148  	}
   149  }
   150  
   151  func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
   152  	groupTeamMapRemoval := "off"
   153  	groupTeamMap := ""
   154  	if len(groupMapParams) == 2 {
   155  		groupTeamMapRemoval = groupMapParams[0]
   156  		groupTeamMap = groupMapParams[1]
   157  	}
   158  	session := loginUser(t, "user1")
   159  	csrf := GetCSRF(t, session, "/admin/auths/new")
   160  	req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval))
   161  	session.MakeRequest(t, req, http.StatusSeeOther)
   162  }
   163  
   164  func TestLDAPUserSignin(t *testing.T) {
   165  	if skipLDAPTests() {
   166  		t.Skip()
   167  		return
   168  	}
   169  	defer tests.PrepareTestEnv(t)()
   170  	addAuthSourceLDAP(t, "", "")
   171  
   172  	u := gitLDAPUsers[0]
   173  
   174  	session := loginUserWithPassword(t, u.UserName, u.Password)
   175  	req := NewRequest(t, "GET", "/user/settings")
   176  	resp := session.MakeRequest(t, req, http.StatusOK)
   177  
   178  	htmlDoc := NewHTMLParser(t, resp.Body)
   179  
   180  	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
   181  	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
   182  	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
   183  }
   184  
   185  func TestLDAPAuthChange(t *testing.T) {
   186  	defer tests.PrepareTestEnv(t)()
   187  	addAuthSourceLDAP(t, "", "")
   188  
   189  	session := loginUser(t, "user1")
   190  	req := NewRequest(t, "GET", "/admin/auths")
   191  	resp := session.MakeRequest(t, req, http.StatusOK)
   192  	doc := NewHTMLParser(t, resp.Body)
   193  	href, exists := doc.Find("table.table td a").Attr("href")
   194  	if !exists {
   195  		assert.True(t, exists, "No authentication source found")
   196  		return
   197  	}
   198  
   199  	req = NewRequest(t, "GET", href)
   200  	resp = session.MakeRequest(t, req, http.StatusOK)
   201  	doc = NewHTMLParser(t, resp.Body)
   202  	csrf := doc.GetCSRF()
   203  	host, _ := doc.Find(`input[name="host"]`).Attr("value")
   204  	assert.Equal(t, host, getLDAPServerHost())
   205  	binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
   206  	assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn)
   207  
   208  	req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off"))
   209  	session.MakeRequest(t, req, http.StatusSeeOther)
   210  
   211  	req = NewRequest(t, "GET", href)
   212  	resp = session.MakeRequest(t, req, http.StatusOK)
   213  	doc = NewHTMLParser(t, resp.Body)
   214  	host, _ = doc.Find(`input[name="host"]`).Attr("value")
   215  	assert.Equal(t, host, getLDAPServerHost())
   216  	binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value")
   217  	assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn)
   218  }
   219  
   220  func TestLDAPUserSync(t *testing.T) {
   221  	if skipLDAPTests() {
   222  		t.Skip()
   223  		return
   224  	}
   225  	defer tests.PrepareTestEnv(t)()
   226  	addAuthSourceLDAP(t, "", "")
   227  	auth.SyncExternalUsers(context.Background(), true)
   228  
   229  	// Check if users exists
   230  	for _, gitLDAPUser := range gitLDAPUsers {
   231  		dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName)
   232  		assert.NoError(t, err)
   233  		assert.Equal(t, gitLDAPUser.UserName, dbUser.Name)
   234  		assert.Equal(t, gitLDAPUser.Email, dbUser.Email)
   235  		assert.Equal(t, gitLDAPUser.IsAdmin, dbUser.IsAdmin)
   236  		assert.Equal(t, gitLDAPUser.IsRestricted, dbUser.IsRestricted)
   237  	}
   238  
   239  	// Check if no users exist
   240  	for _, otherLDAPUser := range otherLDAPUsers {
   241  		_, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName)
   242  		assert.True(t, user_model.IsErrUserNotExist(err))
   243  	}
   244  }
   245  
   246  func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
   247  	if skipLDAPTests() {
   248  		t.Skip()
   249  		return
   250  	}
   251  	defer tests.PrepareTestEnv(t)()
   252  
   253  	session := loginUser(t, "user1")
   254  	csrf := GetCSRF(t, session, "/admin/auths/new")
   255  	payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "")
   256  	payload["attribute_username"] = ""
   257  	req := NewRequestWithValues(t, "POST", "/admin/auths/new", payload)
   258  	session.MakeRequest(t, req, http.StatusSeeOther)
   259  
   260  	for _, u := range gitLDAPUsers {
   261  		req := NewRequest(t, "GET", "/admin/users?q="+u.UserName)
   262  		resp := session.MakeRequest(t, req, http.StatusOK)
   263  
   264  		htmlDoc := NewHTMLParser(t, resp.Body)
   265  
   266  		tr := htmlDoc.doc.Find("table.table tbody tr")
   267  		assert.True(t, tr.Length() == 0)
   268  	}
   269  
   270  	for _, u := range gitLDAPUsers {
   271  		req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
   272  			"_csrf":     csrf,
   273  			"user_name": u.UserName,
   274  			"password":  u.Password,
   275  		})
   276  		MakeRequest(t, req, http.StatusSeeOther)
   277  	}
   278  
   279  	auth.SyncExternalUsers(context.Background(), true)
   280  
   281  	authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
   282  		Name: payload["name"],
   283  	})
   284  	unittest.AssertCount(t, &user_model.User{
   285  		LoginType:   auth_model.LDAP,
   286  		LoginSource: authSource.ID,
   287  	}, len(gitLDAPUsers))
   288  
   289  	for _, u := range gitLDAPUsers {
   290  		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   291  			Name: u.UserName,
   292  		})
   293  		assert.True(t, user.IsActive)
   294  	}
   295  }
   296  
   297  func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
   298  	if skipLDAPTests() {
   299  		t.Skip()
   300  		return
   301  	}
   302  	defer tests.PrepareTestEnv(t)()
   303  	addAuthSourceLDAP(t, "", "(cn=git)")
   304  
   305  	// Assert a user not a member of the LDAP group "cn=git" cannot login
   306  	// This test may look like TestLDAPUserSigninFailed but it is not.
   307  	// The later test uses user filter containing group membership filter (memberOf)
   308  	// This test is for the case when LDAP user records may not be linked with
   309  	// all groups the user is a member of, the user filter is modified accordingly inside
   310  	// the addAuthSourceLDAP based on the value of the groupFilter
   311  	u := otherLDAPUsers[0]
   312  	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
   313  
   314  	auth.SyncExternalUsers(context.Background(), true)
   315  
   316  	// Assert members of LDAP group "cn=git" are added
   317  	for _, gitLDAPUser := range gitLDAPUsers {
   318  		unittest.BeanExists(t, &user_model.User{
   319  			Name: gitLDAPUser.UserName,
   320  		})
   321  	}
   322  
   323  	// Assert everyone else is not added
   324  	for _, gitLDAPUser := range otherLDAPUsers {
   325  		unittest.AssertNotExistsBean(t, &user_model.User{
   326  			Name: gitLDAPUser.UserName,
   327  		})
   328  	}
   329  
   330  	ldapSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
   331  		Name: "ldap",
   332  	})
   333  	ldapConfig := ldapSource.Cfg.(*ldap.Source)
   334  	ldapConfig.GroupFilter = "(cn=ship_crew)"
   335  	auth_model.UpdateSource(ldapSource)
   336  
   337  	auth.SyncExternalUsers(context.Background(), true)
   338  
   339  	for _, gitLDAPUser := range gitLDAPUsers {
   340  		if gitLDAPUser.UserName == "fry" || gitLDAPUser.UserName == "leela" || gitLDAPUser.UserName == "bender" {
   341  			// Assert members of the LDAP group "cn-ship_crew" are still active
   342  			user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   343  				Name: gitLDAPUser.UserName,
   344  			})
   345  			assert.True(t, user.IsActive, "User %s should be active", gitLDAPUser.UserName)
   346  		} else {
   347  			// Assert everyone else is inactive
   348  			user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   349  				Name: gitLDAPUser.UserName,
   350  			})
   351  			assert.False(t, user.IsActive, "User %s should be inactive", gitLDAPUser.UserName)
   352  		}
   353  	}
   354  }
   355  
   356  func TestLDAPUserSigninFailed(t *testing.T) {
   357  	if skipLDAPTests() {
   358  		t.Skip()
   359  		return
   360  	}
   361  	defer tests.PrepareTestEnv(t)()
   362  	addAuthSourceLDAP(t, "", "")
   363  
   364  	u := otherLDAPUsers[0]
   365  	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
   366  }
   367  
   368  func TestLDAPUserSSHKeySync(t *testing.T) {
   369  	if skipLDAPTests() {
   370  		t.Skip()
   371  		return
   372  	}
   373  	defer tests.PrepareTestEnv(t)()
   374  	addAuthSourceLDAP(t, "sshPublicKey", "")
   375  
   376  	auth.SyncExternalUsers(context.Background(), true)
   377  
   378  	// Check if users has SSH keys synced
   379  	for _, u := range gitLDAPUsers {
   380  		if len(u.SSHKeys) == 0 {
   381  			continue
   382  		}
   383  		session := loginUserWithPassword(t, u.UserName, u.Password)
   384  
   385  		req := NewRequest(t, "GET", "/user/settings/keys")
   386  		resp := session.MakeRequest(t, req, http.StatusOK)
   387  
   388  		htmlDoc := NewHTMLParser(t, resp.Body)
   389  
   390  		divs := htmlDoc.doc.Find("#keys-ssh .flex-item .flex-item-body:not(:last-child)")
   391  
   392  		syncedKeys := make([]string, divs.Length())
   393  		for i := 0; i < divs.Length(); i++ {
   394  			syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text())
   395  		}
   396  
   397  		assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName)
   398  	}
   399  }
   400  
   401  func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
   402  	if skipLDAPTests() {
   403  		t.Skip()
   404  		return
   405  	}
   406  	defer tests.PrepareTestEnv(t)()
   407  	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"]}}`)
   408  	org, err := organization.GetOrgByName(db.DefaultContext, "org26")
   409  	assert.NoError(t, err)
   410  	team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
   411  	assert.NoError(t, err)
   412  	auth.SyncExternalUsers(context.Background(), true)
   413  	for _, gitLDAPUser := range gitLDAPUsers {
   414  		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   415  			Name: gitLDAPUser.UserName,
   416  		})
   417  		usersOrgs, err := organization.FindOrgs(organization.FindOrgOptions{
   418  			UserID:         user.ID,
   419  			IncludePrivate: true,
   420  		})
   421  		assert.NoError(t, err)
   422  		allOrgTeams, err := organization.GetUserOrgTeams(db.DefaultContext, org.ID, user.ID)
   423  		assert.NoError(t, err)
   424  		if user.Name == "fry" || user.Name == "leela" || user.Name == "bender" {
   425  			// assert members of LDAP group "cn=ship_crew" are added to mapped teams
   426  			assert.Len(t, usersOrgs, 1, "User [%s] should be member of one organization", user.Name)
   427  			assert.Equal(t, "org26", usersOrgs[0].Name, "Membership should be added to the right organization")
   428  			isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
   429  			assert.NoError(t, err)
   430  			assert.True(t, isMember, "Membership should be added to the right team")
   431  			err = models.RemoveTeamMember(db.DefaultContext, team, user.ID)
   432  			assert.NoError(t, err)
   433  			err = models.RemoveOrgUser(usersOrgs[0].ID, user.ID)
   434  			assert.NoError(t, err)
   435  		} else {
   436  			// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
   437  			assert.Empty(t, usersOrgs, "User should be member of no organization")
   438  			isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
   439  			assert.NoError(t, err)
   440  			assert.False(t, isMember, "User should no be added to this team")
   441  			assert.Empty(t, allOrgTeams, "User should not be added to any team")
   442  		}
   443  	}
   444  }
   445  
   446  func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
   447  	if skipLDAPTests() {
   448  		t.Skip()
   449  		return
   450  	}
   451  	defer tests.PrepareTestEnv(t)()
   452  	addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
   453  	org, err := organization.GetOrgByName(db.DefaultContext, "org26")
   454  	assert.NoError(t, err)
   455  	team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
   456  	assert.NoError(t, err)
   457  	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
   458  	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   459  		Name: gitLDAPUsers[0].UserName,
   460  	})
   461  	err = organization.AddOrgUser(org.ID, user.ID)
   462  	assert.NoError(t, err)
   463  	err = models.AddTeamMember(db.DefaultContext, team, user.ID)
   464  	assert.NoError(t, err)
   465  	isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
   466  	assert.NoError(t, err)
   467  	assert.True(t, isMember, "User should be member of this organization")
   468  	isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
   469  	assert.NoError(t, err)
   470  	assert.True(t, isMember, "User should be member of this team")
   471  	// assert team member "professor" gets removed from org26 team11
   472  	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
   473  	isMember, err = organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
   474  	assert.NoError(t, err)
   475  	assert.False(t, isMember, "User membership should have been removed from organization")
   476  	isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
   477  	assert.NoError(t, err)
   478  	assert.False(t, isMember, "User membership should have been removed from team")
   479  }
   480  
   481  func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
   482  	if skipLDAPTests() {
   483  		t.Skip()
   484  		return
   485  	}
   486  	defer tests.PrepareTestEnv(t)()
   487  
   488  	session := loginUser(t, "user1")
   489  	csrf := GetCSRF(t, session, "/admin/auths/new")
   490  	req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off"))
   491  	session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
   492  }