You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auth_ldap_test.go 16KB

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