-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Add LDAP group sync to Teams, fixes #1395 #16299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
136c628
673df99
3a032cc
6ef0722
8339cf9
76bb588
edd19e2
ed0bab6
eda55b6
5f6f092
ba93eb0
4d864b8
564b59f
1849924
8865932
6d21c2b
f8d7a39
c03bcb7
675d64d
7f6d010
9798db1
a75516d
0d402cc
de1fd67
6ef197e
9563483
c965872
d01e377
82d0cb3
dac97ff
8f0b40a
25880d3
f65f28f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright 2021 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package util | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
) | ||
|
||
// GetKeys returns a slice of keys from a map, dict must be a map | ||
func GetKeys(dict interface{}) interface{} { | ||
value := reflect.ValueOf(dict) | ||
valueType := value.Type() | ||
if value.Kind() == reflect.Map { | ||
keys := value.MapKeys() | ||
length := len(keys) | ||
resultSlice := reflect.MakeSlice(reflect.SliceOf(valueType.Key()), length, length) | ||
for i, key := range keys { | ||
resultSlice.Index(i).Set(key) | ||
} | ||
return resultSlice.Interface() | ||
} | ||
panic(fmt.Sprintf("Type %s is not supported by GetKeys", valueType.String())) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Copyright 2021 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package util | ||
|
||
// IntersectString returns the intersection of the two string slices | ||
func IntersectString(a, b []string) []string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may not be the best algorithm depending on how big these []string get, whether they can be or are sorted and how much of an intersection they have. If the slices are presorted you can generate an intersection in len(a)+len(b) time. The main question is how big a []string we're expecting here. |
||
var intersection []string | ||
for _, v := range a { | ||
if IsStringInSlice(v, b) && !IsStringInSlice(v, intersection) { | ||
intersection = append(intersection, v) | ||
} | ||
} | ||
return intersection | ||
} | ||
|
||
// DifferenceString returns all elements of slice a which are not present in slice b | ||
func DifferenceString(a, b []string) []string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similarly. |
||
var difference []string | ||
for _, v := range a { | ||
if !IsStringInSlice(v, b) && !IsStringInSlice(v, difference) { | ||
difference = append(difference, v) | ||
} | ||
} | ||
return difference | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,9 @@ type Source struct { | |
GroupMemberUID string // Group Attribute containing array of UserUID | ||
UserUID string // User Attribute listed in Group | ||
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source | ||
TeamGroupMap string // Map LDAP groups to teams | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One slight fly in the ointment here is that the login_source will be read and loaded from the source quite a lot - it's infact in the DB as a JSON where it gets unmarshaled on load. |
||
TeamGroupMapRemoval bool // Remove user from teams which are synchronized and user is not a member of the corresponding LDAP group | ||
TeamGroupMapEnabled bool // if LDAP groups mapping to gitea organizations teams is enabled | ||
|
||
// reference to the loginSource | ||
loginSource *login.Source | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// Copyright 2021 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package ldap | ||
|
||
import ( | ||
"code.gitea.io/gitea/models" | ||
"code.gitea.io/gitea/modules/log" | ||
) | ||
|
||
// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships | ||
func (source *Source) SyncLdapGroupsToTeams(user *models.User, ldapTeamAdd map[string][]string, ldapTeamRemove map[string][]string) { | ||
if source.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 := models.GetOrgByName(orgName) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may need to think about caching Orgs and Teams here and in the removeMappedMembership. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I've added a cache for orgs and teams, that is accessed during the external user sync here |
||
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 := models.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) | ||
continue | ||
} | ||
} | ||
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 := models.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) | ||
} else { | ||
continue | ||
} | ||
err = team.AddMember(user.ID) | ||
6543 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
log.Error("LDAP group sync: Could not add user to team: %v", err) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// 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 *models.User, ldapTeamRemove map[string][]string) { | ||
for orgName, teamNames := range ldapTeamRemove { | ||
org, err := models.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 := models.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) | ||
} else { | ||
continue | ||
} | ||
err = team.RemoveMember(user.ID) | ||
6543 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
log.Error("LDAP group sync: Could not remove user from team: %v", err) | ||
} | ||
} | ||
if remainingTeams, err := models.GetUserOrgTeams(org.ID, user.ID); err == nil && len(remainingTeams) == 0 { | ||
// only remove organization membership when no team memberships are left for this organization | ||
6543 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if isMember, err := models.IsOrganizationMember(org.ID, user.ID); isMember && err == nil { | ||
log.Trace("LDAP group sync: removing user [%s] from organization [%s]", user.Name, org.Name) | ||
} else { | ||
continue | ||
} | ||
err = org.RemoveMember(user.ID) | ||
6543 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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)