Skip to content

Commit 524885d

Browse files
lafriksbkcsoft
authored andcommitted
LDAP user synchronization (#1478)
1 parent fd76f09 commit 524885d

File tree

15 files changed

+355
-51
lines changed

15 files changed

+355
-51
lines changed

conf/app.ini

+10
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ SCHEDULE = @every 24h
442442
; Archives created more than OLDER_THAN ago are subject to deletion
443443
OLDER_THAN = 24h
444444

445+
; Synchronize external user data (only LDAP user synchronization is supported)
446+
[cron.sync_external_users]
447+
; Syncronize external user data when starting server (default false)
448+
RUN_AT_START = false
449+
; Interval as a duration between each synchronization (default every 24h)
450+
SCHEDULE = @every 24h
451+
; Create new users, update existing user data and disable users that are not in external source anymore (default)
452+
; or only create new users if UPDATE_EXISTING is set to false
453+
UPDATE_EXISTING = true
454+
445455
[git]
446456
; Disables highlight of added and removed changes
447457
DISABLE_DIFF_HIGHLIGHT = false

models/login_source.go

+23-18
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) {
140140

141141
// LoginSource represents an external way for authorizing users.
142142
type LoginSource struct {
143-
ID int64 `xorm:"pk autoincr"`
144-
Type LoginType
145-
Name string `xorm:"UNIQUE"`
146-
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
147-
Cfg core.Conversion `xorm:"TEXT"`
143+
ID int64 `xorm:"pk autoincr"`
144+
Type LoginType
145+
Name string `xorm:"UNIQUE"`
146+
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
147+
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
148+
Cfg core.Conversion `xorm:"TEXT"`
148149

149150
Created time.Time `xorm:"-"`
150151
CreatedUnix int64 `xorm:"INDEX"`
@@ -294,6 +295,10 @@ func CreateLoginSource(source *LoginSource) error {
294295
} else if has {
295296
return ErrLoginSourceAlreadyExist{source.Name}
296297
}
298+
// Synchronization is only aviable with LDAP for now
299+
if !source.IsLDAP() {
300+
source.IsSyncEnabled = false
301+
}
297302

298303
_, err = x.Insert(source)
299304
if err == nil && source.IsOAuth2() && source.IsActived {
@@ -405,8 +410,8 @@ func composeFullName(firstname, surname, username string) string {
405410
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
406411
// and create a local user if success when enabled.
407412
func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
408-
username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
409-
if !succeed {
413+
sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
414+
if sr == nil {
410415
// User not in LDAP, do nothing
411416
return nil, ErrUserNotExist{0, login, 0}
412417
}
@@ -416,28 +421,28 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
416421
}
417422

418423
// Fallback.
419-
if len(username) == 0 {
420-
username = login
424+
if len(sr.Username) == 0 {
425+
sr.Username = login
421426
}
422427
// Validate username make sure it satisfies requirement.
423-
if binding.AlphaDashDotPattern.MatchString(username) {
424-
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username)
428+
if binding.AlphaDashDotPattern.MatchString(sr.Username) {
429+
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username)
425430
}
426431

427-
if len(mail) == 0 {
428-
mail = fmt.Sprintf("%s@localhost", username)
432+
if len(sr.Mail) == 0 {
433+
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
429434
}
430435

431436
user = &User{
432-
LowerName: strings.ToLower(username),
433-
Name: username,
434-
FullName: composeFullName(fn, sn, username),
435-
Email: mail,
437+
LowerName: strings.ToLower(sr.Username),
438+
Name: sr.Username,
439+
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
440+
Email: sr.Mail,
436441
LoginType: source.Type,
437442
LoginSource: source.ID,
438443
LoginName: login,
439444
IsActive: true,
440-
IsAdmin: isAdmin,
445+
IsAdmin: sr.IsAdmin,
441446
}
442447
return user, CreateUser(user)
443448
}

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ var migrations = []Migration{
110110
NewMigration("add commit status table", addCommitStatus),
111111
// v30 -> 31
112112
NewMigration("add primary key to external login user", addExternalLoginUserPK),
113+
// 31 -> 32
114+
NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
113115
}
114116

115117
// Migrate database to current version

models/migrations/v31.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2017 The Gogs Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
"time"
10+
11+
"github.com/go-xorm/core"
12+
"github.com/go-xorm/xorm"
13+
)
14+
15+
func addLoginSourceSyncEnabledColumn(x *xorm.Engine) error {
16+
// LoginSource see models/login_source.go
17+
type LoginSource struct {
18+
ID int64 `xorm:"pk autoincr"`
19+
Type int
20+
Name string `xorm:"UNIQUE"`
21+
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
22+
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
23+
Cfg core.Conversion `xorm:"TEXT"`
24+
25+
Created time.Time `xorm:"-"`
26+
CreatedUnix int64 `xorm:"INDEX"`
27+
Updated time.Time `xorm:"-"`
28+
UpdatedUnix int64 `xorm:"INDEX"`
29+
}
30+
31+
if err := x.Sync2(new(LoginSource)); err != nil {
32+
return fmt.Errorf("Sync2: %v", err)
33+
}
34+
return nil
35+
}

models/user.go

+127
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const (
5050
UserTypeOrganization
5151
)
5252

53+
const syncExternalUsers = "sync_external_users"
54+
5355
var (
5456
// ErrUserNotKeyOwner user does not own this key error
5557
ErrUserNotKeyOwner = errors.New("User does not own this public key")
@@ -1322,3 +1324,128 @@ func GetWatchedRepos(userID int64, private bool) ([]*Repository, error) {
13221324
}
13231325
return repos, nil
13241326
}
1327+
1328+
// SyncExternalUsers is used to synchronize users with external authorization source
1329+
func SyncExternalUsers() {
1330+
if taskStatusTable.IsRunning(syncExternalUsers) {
1331+
return
1332+
}
1333+
taskStatusTable.Start(syncExternalUsers)
1334+
defer taskStatusTable.Stop(syncExternalUsers)
1335+
1336+
log.Trace("Doing: SyncExternalUsers")
1337+
1338+
ls, err := LoginSources()
1339+
if err != nil {
1340+
log.Error(4, "SyncExternalUsers: %v", err)
1341+
return
1342+
}
1343+
1344+
updateExisting := setting.Cron.SyncExternalUsers.UpdateExisting
1345+
1346+
for _, s := range ls {
1347+
if !s.IsActived || !s.IsSyncEnabled {
1348+
continue
1349+
}
1350+
if s.IsLDAP() {
1351+
log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
1352+
1353+
var existingUsers []int64
1354+
1355+
// Find all users with this login type
1356+
var users []User
1357+
x.Where("login_type = ?", LoginLDAP).
1358+
And("login_source = ?", s.ID).
1359+
Find(&users)
1360+
1361+
sr := s.LDAP().SearchEntries()
1362+
1363+
for _, su := range sr {
1364+
if len(su.Username) == 0 {
1365+
continue
1366+
}
1367+
1368+
if len(su.Mail) == 0 {
1369+
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
1370+
}
1371+
1372+
var usr *User
1373+
// Search for existing user
1374+
for _, du := range users {
1375+
if du.LowerName == strings.ToLower(su.Username) {
1376+
usr = &du
1377+
break
1378+
}
1379+
}
1380+
1381+
fullName := composeFullName(su.Name, su.Surname, su.Username)
1382+
// If no existing user found, create one
1383+
if usr == nil {
1384+
log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
1385+
1386+
usr = &User{
1387+
LowerName: strings.ToLower(su.Username),
1388+
Name: su.Username,
1389+
FullName: fullName,
1390+
LoginType: s.Type,
1391+
LoginSource: s.ID,
1392+
LoginName: su.Username,
1393+
Email: su.Mail,
1394+
IsAdmin: su.IsAdmin,
1395+
IsActive: true,
1396+
}
1397+
1398+
err = CreateUser(usr)
1399+
if err != nil {
1400+
log.Error(4, "SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
1401+
}
1402+
} else if updateExisting {
1403+
existingUsers = append(existingUsers, usr.ID)
1404+
// Check if user data has changed
1405+
if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
1406+
strings.ToLower(usr.Email) != strings.ToLower(su.Mail) ||
1407+
usr.FullName != fullName ||
1408+
!usr.IsActive {
1409+
1410+
log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
1411+
1412+
usr.FullName = fullName
1413+
usr.Email = su.Mail
1414+
// Change existing admin flag only if AdminFilter option is set
1415+
if len(s.LDAP().AdminFilter) > 0 {
1416+
usr.IsAdmin = su.IsAdmin
1417+
}
1418+
usr.IsActive = true
1419+
1420+
err = UpdateUser(usr)
1421+
if err != nil {
1422+
log.Error(4, "SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
1423+
}
1424+
}
1425+
}
1426+
}
1427+
1428+
// Deactivate users not present in LDAP
1429+
if updateExisting {
1430+
for _, usr := range users {
1431+
found := false
1432+
for _, uid := range existingUsers {
1433+
if usr.ID == uid {
1434+
found = true
1435+
break
1436+
}
1437+
}
1438+
if !found {
1439+
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
1440+
1441+
usr.IsActive = false
1442+
err = UpdateUser(&usr)
1443+
if err != nil {
1444+
log.Error(4, "SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
1445+
}
1446+
}
1447+
}
1448+
}
1449+
}
1450+
}
1451+
}

modules/auth/auth_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type AuthenticationForm struct {
2828
Filter string
2929
AdminFilter string
3030
IsActive bool
31+
IsSyncEnabled bool
3132
SMTPAuth string
3233
SMTPHost string
3334
SMTPPort int

0 commit comments

Comments
 (0)