Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
136c628
Add LDAP group sync to Teams, fixes #1395
svenseeberg May 30, 2021
673df99
Add tests to LDAP group sync
melegiul Jul 2, 2021
3a032cc
Replace funk package by custom utility
melegiul Aug 26, 2021
6ef0722
Merge branch 'main' into feature/ldap-group-sync
melegiul Aug 30, 2021
8339cf9
Clean up test database - revert initial
melegiul Aug 31, 2021
76bb588
Skip adding team/org members when already member
melegiul Sep 2, 2021
edd19e2
Rename generic get keys from map function
melegiul Sep 2, 2021
ed0bab6
Merge branch 'main' into feature/ldap-group-sync
melegiul Sep 7, 2021
eda55b6
Merge branch 'main' into feature/ldap-group-sync
melegiul Sep 30, 2021
5f6f092
Merge branch 'main' into feature/ldap-group-sync
melegiul Oct 27, 2021
ba93eb0
Improve non-idiomatic go code
melegiul Oct 27, 2021
4d864b8
Add cache for teams and orgs
melegiul Nov 2, 2021
564b59f
Merge branch 'main' into feature/ldap-group-sync
melegiul Nov 11, 2021
1849924
Fix cli command flag and checkbox listener
melegiul Nov 11, 2021
8865932
Merge branch 'main' into feature/ldap-group-sync
melegiul Dec 14, 2021
6d21c2b
Set log level to warning for missing orgs/teams
melegiul Dec 14, 2021
f8d7a39
Remove redundant check remaining team memberships
melegiul Dec 14, 2021
c03bcb7
Fix integration tests
melegiul Dec 14, 2021
675d64d
Disable group mapping checkbox on LDAP removal
melegiul Dec 14, 2021
7f6d010
Merge branch 'main' into feature/ldap-group-sync
melegiul Jan 19, 2022
9798db1
Merge branch 'main' into feature/ldap-group-sync
Jan 22, 2022
a75516d
Run make fmt
Jan 22, 2022
0d402cc
use kebap case for CSS classes
svenseeberg Jan 22, 2022
de1fd67
Merge branch 'main' into feature/ldap-group-sync
wxiaoguang Feb 8, 2022
6ef197e
refactor
wxiaoguang Feb 8, 2022
9563483
Merge pull request #4 from wxiaoguang/feature/ldap-group-sync
svenseeberg Feb 10, 2022
c965872
Merge branch 'main' into feature/ldap-group-sync
wxiaoguang Feb 10, 2022
d01e377
fix lint
wxiaoguang Feb 10, 2022
82d0cb3
try to fix unit test
wxiaoguang Feb 10, 2022
dac97ff
Merge branch 'main' into feature/ldap-group-sync
6543 Feb 10, 2022
8f0b40a
fix unit test
wxiaoguang Feb 11, 2022
25880d3
Merge branch 'main' into feature/ldap-group-sync
wxiaoguang Feb 11, 2022
f65f28f
Merge branch 'main' into feature/ldap-group-sync
wxiaoguang Feb 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/admin_auth_ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ var (
Name: "public-ssh-key-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
},
cli.StringFlag{
Name: "team-group-map",
Usage: "Map of LDAP groups to teams.",
},
cli.StringFlag{
Name: "team-group-map-force",
Usage: "Force synchronization of mapped LDAP groups to teams.",
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be team-group-map-removal?

Copy link

@melegiul melegiul Nov 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right, I renamed the flag and set its type to boolean (1849924)

}

ldapBindDnCLIFlags = append(commonLdapCLIFlags,
Expand Down Expand Up @@ -245,6 +253,12 @@ func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
if c.IsSet("allow-deactivate-all") {
config.Source.AllowDeactivateAll = c.Bool("allow-deactivate-all")
}
if c.IsSet("team-group-map") {
config.Source.TeamGroupMap = c.String("team-group-map")
}
if c.IsSet("team-group-map-removal") {
config.Source.TeamGroupMapRemoval = c.Bool("team-group-map-removal")
}
return nil
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.7.0
github.com/syndtr/goleveldb v1.0.0
github.com/thoas/go-funk v0.8.0
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/unknwon/com v1.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/thoas/go-funk v0.8.0 h1:JP9tKSvnpFVclYgDM0Is7FD9M4fhPvqA0s0BsXmzSRQ=
github.com/thoas/go-funk v0.8.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
Expand Down
134 changes: 134 additions & 0 deletions integrations/auth_ldap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) {
"attribute_ssh_public_key": sshKeyAttribute,
"is_sync_enabled": "on",
"is_active": "on",
"team_group_map_enabled": "on",
"team_group_map_removal": "on",
"group_dn": "ou=people,dc=planetexpress,dc=com",
"group_member_uid": "member",
"user_uid": "DN",
"team_group_map": "{\"cn=ship_crew,ou=people,dc=planetexpress,dc=com\": {\"org26\": [\"team11\"]},\"cn=admin_staff,ou=people,dc=planetexpress,dc=com\": {\"non-existent\": [\"non-existent\"]},\"cn=non-existent,ou=people,dc=planetexpress,dc=com\": {\"non-existent\": [\"non-existent\"]}}",
})
session.MakeRequest(t, req, http.StatusFound)
}
Expand Down Expand Up @@ -240,3 +246,131 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
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, "")
org, err := models.GetOrgByName("org26")
assert.NoError(t, err)
team, err := models.GetTeam(org.ID, "team11")
assert.NoError(t, err)
models.SyncExternalUsers(context.Background(), true)
for _, gitLDAPUser := range gitLDAPUsers {
user := models.AssertExistsAndLoadBean(t, &models.User{
Name: gitLDAPUser.UserName,
}).(*models.User)
usersOrgs, err := models.GetOrgsByUserID(user.ID, 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 should be member of one organization")
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)
} 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, "")
models.SyncExternalUsers(context.Background(), true)
org, err := models.GetOrgByName("org26")
assert.NoError(t, err)
team, err := models.GetTeam(org.ID, "team11")
assert.NoError(t, err)
user, err := models.GetUserByName("professor")
assert.NoError(t, err)
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 "team11"
models.SyncExternalUsers(context.Background(), true)
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")
}

func addBrokenLDAPMapAuthSource(t *testing.T, sshKeyAttribute string) {
session := loginUser(t, "user1")
csrf := GetCSRF(t, session, "/admin/auths/new")
req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
"_csrf": csrf,
"type": "2",
"name": "ldap",
"host": getLDAPServerHost(),
"port": "389",
"bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com",
"bind_password": "password",
"user_base": "ou=people,dc=planetexpress,dc=com",
"filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
"admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
"restricted_filter": "(uid=leela)",
"attribute_username": "uid",
"attribute_name": "givenName",
"attribute_surname": "sn",
"attribute_mail": "mail",
"attribute_ssh_public_key": sshKeyAttribute,
"is_sync_enabled": "on",
"is_active": "on",
"team_group_map_enabled": "on",
"team_group_map_removal": "on",
"group_dn": "ou=people,dc=planetexpress,dc=com",
"group_member_uid": "member",
"user_uid": "DN",
"team_group_map": "{\"NOT_A_VALID_JSON\"[\"MISSING_DOUBLE_POINT\"]}",
})
session.MakeRequest(t, req, http.StatusFound)
}

// 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)()
addBrokenLDAPMapAuthSource(t, "")

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())
}
86 changes: 84 additions & 2 deletions models/login_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,46 @@ func composeFullName(firstname, surname, username string) string {
}
}

// 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, ldapTeamRemove map[string][]string) {
for orgName, teamNames := range ldapTeamRemove {
org, err := GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Debug("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
for _, teamName := range teamNames {
team, err := org.GetTeam(teamName)
if err != nil {
// team must must be created before LDAP group sync
log.Debug("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
if isMember, err := 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)
}
err = team.RemoveMember(user.ID)
if err != nil {
log.Error("LDAP group sync: Could not remove user from team: %v", err)
}
}
if remainingTeams, err := GetUserOrgTeams(org.ID, user.ID); err == nil && len(remainingTeams) == 0 {
if isMember, err := IsOrganizationMember(org.ID, user.ID); isMember && err == nil {
log.Trace("LDAP group sync: removing user [%s] from organization [%s]", user.Name, org.Name)
}
err = org.RemoveMember(user.ID)
if err != nil {
log.Error("LDAP group sync: Could not remove user from organization: %v", err)
}
} else if err != nil {
log.Error("LDAP group sync: Could not find users [id: %d] teams for given organization [%s]", user.ID, org.Name)
}
}
}

// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
// and create a local user if success when enabled.
func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) {
Expand Down Expand Up @@ -537,7 +577,9 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*Use
if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
return user, RewriteAllPublicKeys()
}

if source.LDAP().TeamGroupMapEnabled || source.LDAP().TeamGroupMapRemoval {
SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, source)
}
return user, nil
}

Expand Down Expand Up @@ -568,10 +610,50 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*Use
if err == nil && isAttributeSSHPublicKeySet && addLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
err = RewriteAllPublicKeys()
}

if source.LDAP().TeamGroupMapEnabled || source.LDAP().TeamGroupMapRemoval {
SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, source)
}
return user, err
}

// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
func SyncLdapGroupsToTeams(user *User, ldapTeamAdd map[string][]string, ldapTeamRemove map[string][]string, source *LoginSource) {
if source.LDAP().TeamGroupMapRemoval {
// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
removeMappedMemberships(user, ldapTeamRemove)
}
for orgName, teamNames := range ldapTeamAdd {
org, err := GetOrgByName(orgName)
if err != nil {
// organization must be created before LDAP group sync
log.Debug("LDAP group sync: Could not find organisation %s: %v", orgName, err)
continue
}
if isMember, err := 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)
}
for _, teamName := range teamNames {
team, err := org.GetTeam(teamName)
if err != nil {
// team must be created before LDAP group sync
log.Debug("LDAP group sync: Could not find team %s: %v", teamName, err)
continue
}
if isMember, err := 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)
}
err = team.AddMember(user.ID)
if err != nil {
log.Error("LDAP group sync: Could not add user to team: %v", err)
}
}
}
}

// _________ __________________________
// / _____/ / \__ ___/\______ \
// \_____ \ / \ / \| | | ___/
Expand Down
4 changes: 4 additions & 0 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,10 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
}
}
}
// Synchronize LDAP groups with organization and team memberships
if s.LDAP().TeamGroupMapEnabled || s.LDAP().TeamGroupMapRemoval {
SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, s)
}
}

// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
Expand Down
8 changes: 8 additions & 0 deletions modules/auth/ldap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,11 @@ share the following fields:
* Group Attribute for User (optional)
* Which group LDAP attribute contains an array above user attribute names.
* 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.
Loading