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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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. // Check if users exists
  205. for _, gitLDAPUser := range gitLDAPUsers {
  206. dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName)
  207. assert.NoError(t, err)
  208. assert.Equal(t, gitLDAPUser.UserName, dbUser.Name)
  209. assert.Equal(t, gitLDAPUser.Email, dbUser.Email)
  210. assert.Equal(t, gitLDAPUser.IsAdmin, dbUser.IsAdmin)
  211. assert.Equal(t, gitLDAPUser.IsRestricted, dbUser.IsRestricted)
  212. }
  213. // Check if no users exist
  214. for _, otherLDAPUser := range otherLDAPUsers {
  215. _, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName)
  216. assert.True(t, user_model.IsErrUserNotExist(err))
  217. }
  218. }
  219. func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
  220. if skipLDAPTests() {
  221. t.Skip()
  222. return
  223. }
  224. defer tests.PrepareTestEnv(t)()
  225. session := loginUser(t, "user1")
  226. csrf := GetCSRF(t, session, "/admin/auths/new")
  227. payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "")
  228. payload["attribute_username"] = ""
  229. req := NewRequestWithValues(t, "POST", "/admin/auths/new", payload)
  230. session.MakeRequest(t, req, http.StatusSeeOther)
  231. for _, u := range gitLDAPUsers {
  232. req := NewRequest(t, "GET", "/admin/users?q="+u.UserName)
  233. resp := session.MakeRequest(t, req, http.StatusOK)
  234. htmlDoc := NewHTMLParser(t, resp.Body)
  235. tr := htmlDoc.doc.Find("table.table tbody tr")
  236. assert.True(t, tr.Length() == 0)
  237. }
  238. for _, u := range gitLDAPUsers {
  239. req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
  240. "_csrf": csrf,
  241. "user_name": u.UserName,
  242. "password": u.Password,
  243. })
  244. MakeRequest(t, req, http.StatusSeeOther)
  245. }
  246. auth.SyncExternalUsers(context.Background(), true)
  247. authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
  248. Name: payload["name"],
  249. })
  250. unittest.AssertCount(t, &user_model.User{
  251. LoginType: auth_model.LDAP,
  252. LoginSource: authSource.ID,
  253. }, len(gitLDAPUsers))
  254. for _, u := range gitLDAPUsers {
  255. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  256. Name: u.UserName,
  257. })
  258. assert.True(t, user.IsActive)
  259. }
  260. }
  261. func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
  262. if skipLDAPTests() {
  263. t.Skip()
  264. return
  265. }
  266. defer tests.PrepareTestEnv(t)()
  267. addAuthSourceLDAP(t, "", "(cn=git)")
  268. // Assert a user not a member of the LDAP group "cn=git" cannot login
  269. // This test may look like TestLDAPUserSigninFailed but it is not.
  270. // The later test uses user filter containing group membership filter (memberOf)
  271. // This test is for the case when LDAP user records may not be linked with
  272. // all groups the user is a member of, the user filter is modified accordingly inside
  273. // the addAuthSourceLDAP based on the value of the groupFilter
  274. u := otherLDAPUsers[0]
  275. testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
  276. auth.SyncExternalUsers(context.Background(), true)
  277. // Assert members of LDAP group "cn=git" are added
  278. for _, gitLDAPUser := range gitLDAPUsers {
  279. unittest.BeanExists(t, &user_model.User{
  280. Name: gitLDAPUser.UserName,
  281. })
  282. }
  283. // Assert everyone else is not added
  284. for _, gitLDAPUser := range otherLDAPUsers {
  285. unittest.AssertNotExistsBean(t, &user_model.User{
  286. Name: gitLDAPUser.UserName,
  287. })
  288. }
  289. ldapSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
  290. Name: "ldap",
  291. })
  292. ldapConfig := ldapSource.Cfg.(*ldap.Source)
  293. ldapConfig.GroupFilter = "(cn=ship_crew)"
  294. auth_model.UpdateSource(ldapSource)
  295. auth.SyncExternalUsers(context.Background(), true)
  296. for _, gitLDAPUser := range gitLDAPUsers {
  297. if gitLDAPUser.UserName == "fry" || gitLDAPUser.UserName == "leela" || gitLDAPUser.UserName == "bender" {
  298. // Assert members of the LDAP group "cn-ship_crew" are still active
  299. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  300. Name: gitLDAPUser.UserName,
  301. })
  302. assert.True(t, user.IsActive, "User %s should be active", gitLDAPUser.UserName)
  303. } else {
  304. // Assert everyone else is inactive
  305. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  306. Name: gitLDAPUser.UserName,
  307. })
  308. assert.False(t, user.IsActive, "User %s should be inactive", gitLDAPUser.UserName)
  309. }
  310. }
  311. }
  312. func TestLDAPUserSigninFailed(t *testing.T) {
  313. if skipLDAPTests() {
  314. t.Skip()
  315. return
  316. }
  317. defer tests.PrepareTestEnv(t)()
  318. addAuthSourceLDAP(t, "", "")
  319. u := otherLDAPUsers[0]
  320. testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
  321. }
  322. func TestLDAPUserSSHKeySync(t *testing.T) {
  323. if skipLDAPTests() {
  324. t.Skip()
  325. return
  326. }
  327. defer tests.PrepareTestEnv(t)()
  328. addAuthSourceLDAP(t, "sshPublicKey", "")
  329. auth.SyncExternalUsers(context.Background(), true)
  330. // Check if users has SSH keys synced
  331. for _, u := range gitLDAPUsers {
  332. if len(u.SSHKeys) == 0 {
  333. continue
  334. }
  335. session := loginUserWithPassword(t, u.UserName, u.Password)
  336. req := NewRequest(t, "GET", "/user/settings/keys")
  337. resp := session.MakeRequest(t, req, http.StatusOK)
  338. htmlDoc := NewHTMLParser(t, resp.Body)
  339. divs := htmlDoc.doc.Find("#keys-ssh .flex-item .flex-item-body:not(:last-child)")
  340. syncedKeys := make([]string, divs.Length())
  341. for i := 0; i < divs.Length(); i++ {
  342. syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text())
  343. }
  344. assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName)
  345. }
  346. }
  347. func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
  348. if skipLDAPTests() {
  349. t.Skip()
  350. return
  351. }
  352. defer tests.PrepareTestEnv(t)()
  353. 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"]}}`)
  354. org, err := organization.GetOrgByName(db.DefaultContext, "org26")
  355. assert.NoError(t, err)
  356. team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
  357. assert.NoError(t, err)
  358. auth.SyncExternalUsers(context.Background(), true)
  359. for _, gitLDAPUser := range gitLDAPUsers {
  360. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  361. Name: gitLDAPUser.UserName,
  362. })
  363. usersOrgs, err := organization.FindOrgs(organization.FindOrgOptions{
  364. UserID: user.ID,
  365. IncludePrivate: true,
  366. })
  367. assert.NoError(t, err)
  368. allOrgTeams, err := organization.GetUserOrgTeams(db.DefaultContext, org.ID, user.ID)
  369. assert.NoError(t, err)
  370. if user.Name == "fry" || user.Name == "leela" || user.Name == "bender" {
  371. // assert members of LDAP group "cn=ship_crew" are added to mapped teams
  372. assert.Len(t, usersOrgs, 1, "User [%s] should be member of one organization", user.Name)
  373. assert.Equal(t, "org26", usersOrgs[0].Name, "Membership should be added to the right organization")
  374. isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
  375. assert.NoError(t, err)
  376. assert.True(t, isMember, "Membership should be added to the right team")
  377. err = models.RemoveTeamMember(team, user.ID)
  378. assert.NoError(t, err)
  379. err = models.RemoveOrgUser(usersOrgs[0].ID, user.ID)
  380. assert.NoError(t, err)
  381. } else {
  382. // assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
  383. assert.Empty(t, usersOrgs, "User should be member of no organization")
  384. isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
  385. assert.NoError(t, err)
  386. assert.False(t, isMember, "User should no be added to this team")
  387. assert.Empty(t, allOrgTeams, "User should not be added to any team")
  388. }
  389. }
  390. }
  391. func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
  392. if skipLDAPTests() {
  393. t.Skip()
  394. return
  395. }
  396. defer tests.PrepareTestEnv(t)()
  397. addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
  398. org, err := organization.GetOrgByName(db.DefaultContext, "org26")
  399. assert.NoError(t, err)
  400. team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
  401. assert.NoError(t, err)
  402. loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
  403. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  404. Name: gitLDAPUsers[0].UserName,
  405. })
  406. err = organization.AddOrgUser(org.ID, user.ID)
  407. assert.NoError(t, err)
  408. err = models.AddTeamMember(team, user.ID)
  409. assert.NoError(t, err)
  410. isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
  411. assert.NoError(t, err)
  412. assert.True(t, isMember, "User should be member of this organization")
  413. isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
  414. assert.NoError(t, err)
  415. assert.True(t, isMember, "User should be member of this team")
  416. // assert team member "professor" gets removed from org26 team11
  417. loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
  418. isMember, err = organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
  419. assert.NoError(t, err)
  420. assert.False(t, isMember, "User membership should have been removed from organization")
  421. isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
  422. assert.NoError(t, err)
  423. assert.False(t, isMember, "User membership should have been removed from team")
  424. }
  425. func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
  426. if skipLDAPTests() {
  427. t.Skip()
  428. return
  429. }
  430. defer tests.PrepareTestEnv(t)()
  431. session := loginUser(t, "user1")
  432. csrf := GetCSRF(t, session, "/admin/auths/new")
  433. req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off"))
  434. session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
  435. }