mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-27 12:16:10 +01:00
e8186f1c0f
Fixes #19555 Test-Instructions: https://github.com/go-gitea/gitea/pull/21441#issuecomment-1419438000 This PR implements the mapping of user groups provided by OIDC providers to orgs teams in Gitea. The main part is a refactoring of the existing LDAP code to make it usable from different providers. Refactorings: - Moved the router auth code from module to service because of import cycles - Changed some model methods to take a `Context` parameter - Moved the mapping code from LDAP to a common location I've tested it with Keycloak but other providers should work too. The JSON mapping format is the same as for LDAP. ![grafik](https://user-images.githubusercontent.com/1666336/195634392-3fc540fc-b229-4649-99ac-91ae8e19df2d.png) --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
224 lines
7.2 KiB
Go
224 lines
7.2 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ldap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/models/organization"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
auth_module "code.gitea.io/gitea/modules/auth"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/util"
|
|
source_service "code.gitea.io/gitea/services/auth/source"
|
|
user_service "code.gitea.io/gitea/services/user"
|
|
)
|
|
|
|
// Sync causes this ldap source to synchronize its users with the db
|
|
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
|
log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name)
|
|
|
|
var existingUsers []int
|
|
isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
|
|
var sshKeysNeedUpdate bool
|
|
|
|
// Find all users with this login type - FIXME: Should this be an iterator?
|
|
users, err := user_model.GetUsersBySource(source.authSource)
|
|
if err != nil {
|
|
log.Error("SyncExternalUsers: %v", err)
|
|
return err
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.authSource.Name)
|
|
return db.ErrCancelledf("Before update of %s", source.authSource.Name)
|
|
default:
|
|
}
|
|
|
|
sort.Slice(users, func(i, j int) bool {
|
|
return users[i].LowerName < users[j].LowerName
|
|
})
|
|
|
|
sr, err := source.SearchEntries()
|
|
if err != nil {
|
|
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.authSource.Name)
|
|
return nil
|
|
}
|
|
|
|
if len(sr) == 0 {
|
|
if !source.AllowDeactivateAll {
|
|
log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
|
|
return nil
|
|
}
|
|
log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
|
|
}
|
|
|
|
sort.Slice(sr, func(i, j int) bool {
|
|
return sr[i].LowerName < sr[j].LowerName
|
|
})
|
|
|
|
userPos := 0
|
|
orgCache := make(map[string]*organization.Organization)
|
|
teamCache := make(map[string]*organization.Team)
|
|
|
|
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, su := range sr {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
|
|
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
|
if sshKeysNeedUpdate {
|
|
err = asymkey_model.RewriteAllPublicKeys()
|
|
if err != nil {
|
|
log.Error("RewriteAllPublicKeys: %v", err)
|
|
}
|
|
}
|
|
return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name)
|
|
default:
|
|
}
|
|
if len(su.Username) == 0 {
|
|
continue
|
|
}
|
|
|
|
if len(su.Mail) == 0 {
|
|
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
|
|
}
|
|
|
|
var usr *user_model.User
|
|
for userPos < len(users) && users[userPos].LowerName < su.LowerName {
|
|
userPos++
|
|
}
|
|
if userPos < len(users) && users[userPos].LowerName == su.LowerName {
|
|
usr = users[userPos]
|
|
existingUsers = append(existingUsers, userPos)
|
|
}
|
|
|
|
fullName := composeFullName(su.Name, su.Surname, su.Username)
|
|
// If no existing user found, create one
|
|
if usr == nil {
|
|
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.authSource.Name, su.Username)
|
|
|
|
usr = &user_model.User{
|
|
LowerName: su.LowerName,
|
|
Name: su.Username,
|
|
FullName: fullName,
|
|
LoginType: source.authSource.Type,
|
|
LoginSource: source.authSource.ID,
|
|
LoginName: su.Username,
|
|
Email: su.Mail,
|
|
IsAdmin: su.IsAdmin,
|
|
}
|
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
|
IsRestricted: util.OptionalBoolOf(su.IsRestricted),
|
|
IsActive: util.OptionalBoolTrue,
|
|
}
|
|
|
|
err = user_model.CreateUser(usr, overwriteDefault)
|
|
if err != nil {
|
|
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
|
|
}
|
|
|
|
if err == nil && isAttributeSSHPublicKeySet {
|
|
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.authSource.Name, usr.Name)
|
|
if asymkey_model.AddPublicKeysBySource(usr, source.authSource, su.SSHPublicKey) {
|
|
sshKeysNeedUpdate = true
|
|
}
|
|
}
|
|
|
|
if err == nil && len(source.AttributeAvatar) > 0 {
|
|
_ = user_service.UploadAvatar(usr, su.Avatar)
|
|
}
|
|
} else if updateExisting {
|
|
// Synchronize SSH Public Key if that attribute is set
|
|
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(usr, source.authSource, su.SSHPublicKey) {
|
|
sshKeysNeedUpdate = true
|
|
}
|
|
|
|
// Check if user data has changed
|
|
if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
|
|
(len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
|
|
!strings.EqualFold(usr.Email, su.Mail) ||
|
|
usr.FullName != fullName ||
|
|
!usr.IsActive {
|
|
|
|
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name)
|
|
|
|
usr.FullName = fullName
|
|
emailChanged := usr.Email != su.Mail
|
|
usr.Email = su.Mail
|
|
// Change existing admin flag only if AdminFilter option is set
|
|
if len(source.AdminFilter) > 0 {
|
|
usr.IsAdmin = su.IsAdmin
|
|
}
|
|
// Change existing restricted flag only if RestrictedFilter option is set
|
|
if !usr.IsAdmin && len(source.RestrictedFilter) > 0 {
|
|
usr.IsRestricted = su.IsRestricted
|
|
}
|
|
usr.IsActive = true
|
|
|
|
err = user_model.UpdateUser(ctx, usr, emailChanged, "full_name", "email", "is_admin", "is_restricted", "is_active")
|
|
if err != nil {
|
|
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err)
|
|
}
|
|
}
|
|
|
|
if usr.IsUploadAvatarChanged(su.Avatar) {
|
|
if err == nil && len(source.AttributeAvatar) > 0 {
|
|
_ = user_service.UploadAvatar(usr, su.Avatar)
|
|
}
|
|
}
|
|
}
|
|
// Synchronize LDAP groups with organization and team memberships
|
|
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
|
if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil {
|
|
log.Error("SyncGroupsToTeamsCached: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
|
|
if sshKeysNeedUpdate {
|
|
err = asymkey_model.RewriteAllPublicKeys()
|
|
if err != nil {
|
|
log.Error("RewriteAllPublicKeys: %v", err)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.authSource.Name)
|
|
return db.ErrCancelledf("During update of %s before delete users", source.authSource.Name)
|
|
default:
|
|
}
|
|
|
|
// Deactivate users not present in LDAP
|
|
if updateExisting {
|
|
existPos := 0
|
|
for i, usr := range users {
|
|
for existPos < len(existingUsers) && i > existingUsers[existPos] {
|
|
existPos++
|
|
}
|
|
if usr.IsActive && (existPos >= len(existingUsers) || i < existingUsers[existPos]) {
|
|
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
|
|
|
|
usr.IsActive = false
|
|
err = user_model.UpdateUserCols(ctx, usr, "is_active")
|
|
if err != nil {
|
|
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|