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 }