LDAP user synchronization (#1478)
This commit is contained in:
parent
fd76f090a2
commit
524885dd65
15 changed files with 356 additions and 52 deletions
10
conf/app.ini
vendored
10
conf/app.ini
vendored
|
@ -442,6 +442,16 @@ SCHEDULE = @every 24h
|
||||||
; Archives created more than OLDER_THAN ago are subject to deletion
|
; Archives created more than OLDER_THAN ago are subject to deletion
|
||||||
OLDER_THAN = 24h
|
OLDER_THAN = 24h
|
||||||
|
|
||||||
|
; Synchronize external user data (only LDAP user synchronization is supported)
|
||||||
|
[cron.sync_external_users]
|
||||||
|
; Syncronize external user data when starting server (default false)
|
||||||
|
RUN_AT_START = false
|
||||||
|
; Interval as a duration between each synchronization (default every 24h)
|
||||||
|
SCHEDULE = @every 24h
|
||||||
|
; Create new users, update existing user data and disable users that are not in external source anymore (default)
|
||||||
|
; or only create new users if UPDATE_EXISTING is set to false
|
||||||
|
UPDATE_EXISTING = true
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
; Disables highlight of added and removed changes
|
; Disables highlight of added and removed changes
|
||||||
DISABLE_DIFF_HIGHLIGHT = false
|
DISABLE_DIFF_HIGHLIGHT = false
|
||||||
|
|
|
@ -144,6 +144,7 @@ type LoginSource struct {
|
||||||
Type LoginType
|
Type LoginType
|
||||||
Name string `xorm:"UNIQUE"`
|
Name string `xorm:"UNIQUE"`
|
||||||
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
|
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
Cfg core.Conversion `xorm:"TEXT"`
|
Cfg core.Conversion `xorm:"TEXT"`
|
||||||
|
|
||||||
Created time.Time `xorm:"-"`
|
Created time.Time `xorm:"-"`
|
||||||
|
@ -294,6 +295,10 @@ func CreateLoginSource(source *LoginSource) error {
|
||||||
} else if has {
|
} else if has {
|
||||||
return ErrLoginSourceAlreadyExist{source.Name}
|
return ErrLoginSourceAlreadyExist{source.Name}
|
||||||
}
|
}
|
||||||
|
// Synchronization is only aviable with LDAP for now
|
||||||
|
if !source.IsLDAP() {
|
||||||
|
source.IsSyncEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
_, err = x.Insert(source)
|
_, err = x.Insert(source)
|
||||||
if err == nil && source.IsOAuth2() && source.IsActived {
|
if err == nil && source.IsOAuth2() && source.IsActived {
|
||||||
|
@ -405,8 +410,8 @@ func composeFullName(firstname, surname, username string) string {
|
||||||
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
|
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
|
||||||
// and create a local user if success when enabled.
|
// and create a local user if success when enabled.
|
||||||
func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
|
||||||
username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
|
sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
|
||||||
if !succeed {
|
if sr == nil {
|
||||||
// User not in LDAP, do nothing
|
// User not in LDAP, do nothing
|
||||||
return nil, ErrUserNotExist{0, login, 0}
|
return nil, ErrUserNotExist{0, login, 0}
|
||||||
}
|
}
|
||||||
|
@ -416,28 +421,28 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback.
|
// Fallback.
|
||||||
if len(username) == 0 {
|
if len(sr.Username) == 0 {
|
||||||
username = login
|
sr.Username = login
|
||||||
}
|
}
|
||||||
// Validate username make sure it satisfies requirement.
|
// Validate username make sure it satisfies requirement.
|
||||||
if binding.AlphaDashDotPattern.MatchString(username) {
|
if binding.AlphaDashDotPattern.MatchString(sr.Username) {
|
||||||
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username)
|
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mail) == 0 {
|
if len(sr.Mail) == 0 {
|
||||||
mail = fmt.Sprintf("%s@localhost", username)
|
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
user = &User{
|
user = &User{
|
||||||
LowerName: strings.ToLower(username),
|
LowerName: strings.ToLower(sr.Username),
|
||||||
Name: username,
|
Name: sr.Username,
|
||||||
FullName: composeFullName(fn, sn, username),
|
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||||
Email: mail,
|
Email: sr.Mail,
|
||||||
LoginType: source.Type,
|
LoginType: source.Type,
|
||||||
LoginSource: source.ID,
|
LoginSource: source.ID,
|
||||||
LoginName: login,
|
LoginName: login,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
IsAdmin: isAdmin,
|
IsAdmin: sr.IsAdmin,
|
||||||
}
|
}
|
||||||
return user, CreateUser(user)
|
return user, CreateUser(user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,8 @@ var migrations = []Migration{
|
||||||
NewMigration("add commit status table", addCommitStatus),
|
NewMigration("add commit status table", addCommitStatus),
|
||||||
// v30 -> 31
|
// v30 -> 31
|
||||||
NewMigration("add primary key to external login user", addExternalLoginUserPK),
|
NewMigration("add primary key to external login user", addExternalLoginUserPK),
|
||||||
|
// 31 -> 32
|
||||||
|
NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
35
models/migrations/v31.go
Normal file
35
models/migrations/v31.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2017 The Gogs 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 migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-xorm/core"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addLoginSourceSyncEnabledColumn(x *xorm.Engine) error {
|
||||||
|
// LoginSource see models/login_source.go
|
||||||
|
type LoginSource struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Type int
|
||||||
|
Name string `xorm:"UNIQUE"`
|
||||||
|
IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
|
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||||
|
Cfg core.Conversion `xorm:"TEXT"`
|
||||||
|
|
||||||
|
Created time.Time `xorm:"-"`
|
||||||
|
CreatedUnix int64 `xorm:"INDEX"`
|
||||||
|
Updated time.Time `xorm:"-"`
|
||||||
|
UpdatedUnix int64 `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync2(new(LoginSource)); err != nil {
|
||||||
|
return fmt.Errorf("Sync2: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
127
models/user.go
127
models/user.go
|
@ -50,6 +50,8 @@ const (
|
||||||
UserTypeOrganization
|
UserTypeOrganization
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const syncExternalUsers = "sync_external_users"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrUserNotKeyOwner user does not own this key error
|
// ErrUserNotKeyOwner user does not own this key error
|
||||||
ErrUserNotKeyOwner = errors.New("User does not own this public key")
|
ErrUserNotKeyOwner = errors.New("User does not own this public key")
|
||||||
|
@ -1322,3 +1324,128 @@ func GetWatchedRepos(userID int64, private bool) ([]*Repository, error) {
|
||||||
}
|
}
|
||||||
return repos, nil
|
return repos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncExternalUsers is used to synchronize users with external authorization source
|
||||||
|
func SyncExternalUsers() {
|
||||||
|
if taskStatusTable.IsRunning(syncExternalUsers) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskStatusTable.Start(syncExternalUsers)
|
||||||
|
defer taskStatusTable.Stop(syncExternalUsers)
|
||||||
|
|
||||||
|
log.Trace("Doing: SyncExternalUsers")
|
||||||
|
|
||||||
|
ls, err := LoginSources()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "SyncExternalUsers: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateExisting := setting.Cron.SyncExternalUsers.UpdateExisting
|
||||||
|
|
||||||
|
for _, s := range ls {
|
||||||
|
if !s.IsActived || !s.IsSyncEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.IsLDAP() {
|
||||||
|
log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
|
||||||
|
|
||||||
|
var existingUsers []int64
|
||||||
|
|
||||||
|
// Find all users with this login type
|
||||||
|
var users []User
|
||||||
|
x.Where("login_type = ?", LoginLDAP).
|
||||||
|
And("login_source = ?", s.ID).
|
||||||
|
Find(&users)
|
||||||
|
|
||||||
|
sr := s.LDAP().SearchEntries()
|
||||||
|
|
||||||
|
for _, su := range sr {
|
||||||
|
if len(su.Username) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(su.Mail) == 0 {
|
||||||
|
su.Mail = fmt.Sprintf("%s@localhost", su.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
var usr *User
|
||||||
|
// Search for existing user
|
||||||
|
for _, du := range users {
|
||||||
|
if du.LowerName == strings.ToLower(su.Username) {
|
||||||
|
usr = &du
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", s.Name, su.Username)
|
||||||
|
|
||||||
|
usr = &User{
|
||||||
|
LowerName: strings.ToLower(su.Username),
|
||||||
|
Name: su.Username,
|
||||||
|
FullName: fullName,
|
||||||
|
LoginType: s.Type,
|
||||||
|
LoginSource: s.ID,
|
||||||
|
LoginName: su.Username,
|
||||||
|
Email: su.Mail,
|
||||||
|
IsAdmin: su.IsAdmin,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CreateUser(usr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
|
||||||
|
}
|
||||||
|
} else if updateExisting {
|
||||||
|
existingUsers = append(existingUsers, usr.ID)
|
||||||
|
// Check if user data has changed
|
||||||
|
if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
|
||||||
|
strings.ToLower(usr.Email) != strings.ToLower(su.Mail) ||
|
||||||
|
usr.FullName != fullName ||
|
||||||
|
!usr.IsActive {
|
||||||
|
|
||||||
|
log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
|
||||||
|
|
||||||
|
usr.FullName = fullName
|
||||||
|
usr.Email = su.Mail
|
||||||
|
// Change existing admin flag only if AdminFilter option is set
|
||||||
|
if len(s.LDAP().AdminFilter) > 0 {
|
||||||
|
usr.IsAdmin = su.IsAdmin
|
||||||
|
}
|
||||||
|
usr.IsActive = true
|
||||||
|
|
||||||
|
err = UpdateUser(usr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate users not present in LDAP
|
||||||
|
if updateExisting {
|
||||||
|
for _, usr := range users {
|
||||||
|
found := false
|
||||||
|
for _, uid := range existingUsers {
|
||||||
|
if usr.ID == uid {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
|
||||||
|
|
||||||
|
usr.IsActive = false
|
||||||
|
err = UpdateUser(&usr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ type AuthenticationForm struct {
|
||||||
Filter string
|
Filter string
|
||||||
AdminFilter string
|
AdminFilter string
|
||||||
IsActive bool
|
IsActive bool
|
||||||
|
IsSyncEnabled bool
|
||||||
SMTPAuth string
|
SMTPAuth string
|
||||||
SMTPHost string
|
SMTPHost string
|
||||||
SMTPPort int
|
SMTPPort int
|
||||||
|
|
|
@ -47,6 +47,15 @@ type Source struct {
|
||||||
Enabled bool // if this source is disabled
|
Enabled bool // if this source is disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchResult : user data
|
||||||
|
type SearchResult struct {
|
||||||
|
Username string // Username
|
||||||
|
Name string // Name
|
||||||
|
Surname string // Surname
|
||||||
|
Mail string // E-mail address
|
||||||
|
IsAdmin bool // if user is administrator
|
||||||
|
}
|
||||||
|
|
||||||
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
|
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
|
||||||
// See http://tools.ietf.org/search/rfc4515
|
// See http://tools.ietf.org/search/rfc4515
|
||||||
badCharacters := "\x00()*\\"
|
badCharacters := "\x00()*\\"
|
||||||
|
@ -149,18 +158,39 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
|
||||||
|
if len(ls.AdminFilter) > 0 {
|
||||||
|
log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
|
||||||
|
search := ldap.NewSearchRequest(
|
||||||
|
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
|
||||||
|
[]string{ls.AttributeName},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
sr, err := l.Search(search)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "LDAP Admin Search failed unexpectedly! (%v)", err)
|
||||||
|
} else if len(sr.Entries) < 1 {
|
||||||
|
log.Error(4, "LDAP Admin Search failed")
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
|
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
|
||||||
func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
|
func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
|
||||||
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
|
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
|
||||||
if len(passwd) == 0 {
|
if len(passwd) == 0 {
|
||||||
log.Debug("Auth. failed for %s, password cannot be empty")
|
log.Debug("Auth. failed for %s, password cannot be empty")
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
l, err := dial(ls)
|
l, err := dial(ls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
|
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
|
||||||
ls.Enabled = false
|
ls.Enabled = false
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
||||||
|
@ -171,7 +201,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
var ok bool
|
var ok bool
|
||||||
userDN, ok = ls.sanitizedUserDN(name)
|
userDN, ok = ls.sanitizedUserDN(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Trace("LDAP will use BindDN.")
|
log.Trace("LDAP will use BindDN.")
|
||||||
|
@ -179,7 +209,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
var found bool
|
var found bool
|
||||||
userDN, found = ls.findUserDN(l, name)
|
userDN, found = ls.findUserDN(l, name)
|
||||||
if !found {
|
if !found {
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,13 +217,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
// binds user (checking password) before looking-up attributes in user context
|
// binds user (checking password) before looking-up attributes in user context
|
||||||
err = bindUser(l, userDN, passwd)
|
err = bindUser(l, userDN, passwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userFilter, ok := ls.sanitizedUserQuery(name)
|
userFilter, ok := ls.sanitizedUserQuery(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, userDN)
|
log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, userDN)
|
||||||
|
@ -205,7 +235,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
sr, err := l.Search(search)
|
sr, err := l.Search(search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
|
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
} else if len(sr.Entries) < 1 {
|
} else if len(sr.Entries) < 1 {
|
||||||
if directBind {
|
if directBind {
|
||||||
log.Error(4, "User filter inhibited user login.")
|
log.Error(4, "User filter inhibited user login.")
|
||||||
|
@ -213,39 +243,78 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
|
||||||
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
|
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
|
username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
|
||||||
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
|
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
|
||||||
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
|
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
|
||||||
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
|
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
|
||||||
|
isAdmin := checkAdmin(l, ls, userDN)
|
||||||
isAdmin := false
|
|
||||||
if len(ls.AdminFilter) > 0 {
|
|
||||||
log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
|
|
||||||
search = ldap.NewSearchRequest(
|
|
||||||
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
|
|
||||||
[]string{ls.AttributeName},
|
|
||||||
nil)
|
|
||||||
|
|
||||||
sr, err = l.Search(search)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(4, "LDAP Admin Search failed unexpectedly! (%v)", err)
|
|
||||||
} else if len(sr.Entries) < 1 {
|
|
||||||
log.Error(4, "LDAP Admin Search failed")
|
|
||||||
} else {
|
|
||||||
isAdmin = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !directBind && ls.AttributesInBind {
|
if !directBind && ls.AttributesInBind {
|
||||||
// binds user (checking password) after looking-up attributes in BindDN context
|
// binds user (checking password) after looking-up attributes in BindDN context
|
||||||
err = bindUser(l, userDN, passwd)
|
err = bindUser(l, userDN, passwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", false, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return username, firstname, surname, mail, isAdmin, true
|
return &SearchResult{
|
||||||
|
Username: username,
|
||||||
|
Name: firstname,
|
||||||
|
Surname: surname,
|
||||||
|
Mail: mail,
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchEntries : search an LDAP source for all users matching userFilter
|
||||||
|
func (ls *Source) SearchEntries() []*SearchResult {
|
||||||
|
l, err := dial(ls)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
|
||||||
|
ls.Enabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
if ls.BindDN != "" && ls.BindPassword != "" {
|
||||||
|
err := l.Bind(ls.BindDN, ls.BindPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Trace("Bound as BindDN %s", ls.BindDN)
|
||||||
|
} else {
|
||||||
|
log.Trace("Proceeding with anonymous LDAP search.")
|
||||||
|
}
|
||||||
|
|
||||||
|
userFilter := fmt.Sprintf(ls.Filter, "*")
|
||||||
|
|
||||||
|
log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, ls.UserBase)
|
||||||
|
search := ldap.NewSearchRequest(
|
||||||
|
ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
|
||||||
|
[]string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
sr, err := l.Search(search)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*SearchResult, len(sr.Entries))
|
||||||
|
|
||||||
|
for i, v := range sr.Entries {
|
||||||
|
result[i] = &SearchResult{
|
||||||
|
Username: v.GetAttributeValue(ls.AttributeUsername),
|
||||||
|
Name: v.GetAttributeValue(ls.AttributeName),
|
||||||
|
Surname: v.GetAttributeValue(ls.AttributeSurname),
|
||||||
|
Mail: v.GetAttributeValue(ls.AttributeMail),
|
||||||
|
IsAdmin: checkAdmin(l, ls, v.DN),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,17 @@ func NewContext() {
|
||||||
go models.DeleteOldRepositoryArchives()
|
go models.DeleteOldRepositoryArchives()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if setting.Cron.SyncExternalUsers.Enabled {
|
||||||
|
entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, models.SyncExternalUsers)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(4, "Cron[Synchronize external users]: %v", err)
|
||||||
|
}
|
||||||
|
if setting.Cron.SyncExternalUsers.RunAtStart {
|
||||||
|
entry.Prev = time.Now()
|
||||||
|
entry.ExecTimes++
|
||||||
|
go models.SyncExternalUsers()
|
||||||
|
}
|
||||||
|
}
|
||||||
c.Start()
|
c.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -336,6 +336,12 @@ var (
|
||||||
Schedule string
|
Schedule string
|
||||||
OlderThan time.Duration
|
OlderThan time.Duration
|
||||||
} `ini:"cron.archive_cleanup"`
|
} `ini:"cron.archive_cleanup"`
|
||||||
|
SyncExternalUsers struct {
|
||||||
|
Enabled bool
|
||||||
|
RunAtStart bool
|
||||||
|
Schedule string
|
||||||
|
UpdateExisting bool
|
||||||
|
} `ini:"cron.sync_external_users"`
|
||||||
}{
|
}{
|
||||||
UpdateMirror: struct {
|
UpdateMirror: struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
@ -379,6 +385,17 @@ var (
|
||||||
Schedule: "@every 24h",
|
Schedule: "@every 24h",
|
||||||
OlderThan: 24 * time.Hour,
|
OlderThan: 24 * time.Hour,
|
||||||
},
|
},
|
||||||
|
SyncExternalUsers: struct {
|
||||||
|
Enabled bool
|
||||||
|
RunAtStart bool
|
||||||
|
Schedule string
|
||||||
|
UpdateExisting bool
|
||||||
|
}{
|
||||||
|
Enabled: true,
|
||||||
|
RunAtStart: false,
|
||||||
|
Schedule: "@every 24h",
|
||||||
|
UpdateExisting: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git settings
|
// Git settings
|
||||||
|
|
|
@ -1065,7 +1065,8 @@ dashboard.resync_all_hooks = Resync pre-receive, update and post-receive hooks o
|
||||||
dashboard.resync_all_hooks_success = All repositories' pre-receive, update and post-receive hooks have been resynced successfully.
|
dashboard.resync_all_hooks_success = All repositories' pre-receive, update and post-receive hooks have been resynced successfully.
|
||||||
dashboard.reinit_missing_repos = Reinitialize all lost Git repositories for which records exist
|
dashboard.reinit_missing_repos = Reinitialize all lost Git repositories for which records exist
|
||||||
dashboard.reinit_missing_repos_success = All lost Git repositories for which records existed have been reinitialized successfully.
|
dashboard.reinit_missing_repos_success = All lost Git repositories for which records existed have been reinitialized successfully.
|
||||||
|
dashboard.sync_external_users = Synchronize external user data
|
||||||
|
dashboard.sync_external_users_started = External user synchronization started
|
||||||
dashboard.server_uptime = Server Uptime
|
dashboard.server_uptime = Server Uptime
|
||||||
dashboard.current_goroutine = Current Goroutines
|
dashboard.current_goroutine = Current Goroutines
|
||||||
dashboard.current_memory_usage = Current Memory Usage
|
dashboard.current_memory_usage = Current Memory Usage
|
||||||
|
@ -1147,6 +1148,7 @@ auths.new = Add New Source
|
||||||
auths.name = Name
|
auths.name = Name
|
||||||
auths.type = Type
|
auths.type = Type
|
||||||
auths.enabled = Enabled
|
auths.enabled = Enabled
|
||||||
|
auths.syncenabled = Enable user synchronization
|
||||||
auths.updated = Updated
|
auths.updated = Updated
|
||||||
auths.auth_type = Authentication Type
|
auths.auth_type = Authentication Type
|
||||||
auths.auth_name = Authentication Name
|
auths.auth_name = Authentication Name
|
||||||
|
|
|
@ -121,6 +121,7 @@ const (
|
||||||
syncSSHAuthorizedKey
|
syncSSHAuthorizedKey
|
||||||
syncRepositoryUpdateHook
|
syncRepositoryUpdateHook
|
||||||
reinitMissingRepository
|
reinitMissingRepository
|
||||||
|
syncExternalUsers
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dashboard show admin panel dashboard
|
// Dashboard show admin panel dashboard
|
||||||
|
@ -157,6 +158,9 @@ func Dashboard(ctx *context.Context) {
|
||||||
case reinitMissingRepository:
|
case reinitMissingRepository:
|
||||||
success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
|
success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
|
||||||
err = models.ReinitMissingRepositories()
|
err = models.ReinitMissingRepositories()
|
||||||
|
case syncExternalUsers:
|
||||||
|
success = ctx.Tr("admin.dashboard.sync_external_users_started")
|
||||||
|
go models.SyncExternalUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -74,6 +74,7 @@ func NewAuthSource(ctx *context.Context) {
|
||||||
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
|
||||||
ctx.Data["smtp_auth"] = "PLAIN"
|
ctx.Data["smtp_auth"] = "PLAIN"
|
||||||
ctx.Data["is_active"] = true
|
ctx.Data["is_active"] = true
|
||||||
|
ctx.Data["is_sync_enabled"] = true
|
||||||
ctx.Data["AuthSources"] = authSources
|
ctx.Data["AuthSources"] = authSources
|
||||||
ctx.Data["SecurityProtocols"] = securityProtocols
|
ctx.Data["SecurityProtocols"] = securityProtocols
|
||||||
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
ctx.Data["SMTPAuths"] = models.SMTPAuths
|
||||||
|
@ -189,6 +190,7 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
|
||||||
Type: models.LoginType(form.Type),
|
Type: models.LoginType(form.Type),
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
IsActived: form.IsActive,
|
IsActived: form.IsActive,
|
||||||
|
IsSyncEnabled: form.IsSyncEnabled,
|
||||||
Cfg: config,
|
Cfg: config,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if models.IsErrLoginSourceAlreadyExist(err) {
|
if models.IsErrLoginSourceAlreadyExist(err) {
|
||||||
|
@ -273,6 +275,7 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
|
||||||
|
|
||||||
source.Name = form.Name
|
source.Name = form.Name
|
||||||
source.IsActived = form.IsActive
|
source.IsActived = form.IsActive
|
||||||
|
source.IsSyncEnabled = form.IsSyncEnabled
|
||||||
source.Cfg = config
|
source.Cfg = config
|
||||||
if err := models.UpdateSource(source); err != nil {
|
if err := models.UpdateSource(source); err != nil {
|
||||||
if models.IsErrOpenIDConnectInitialize(err) {
|
if models.IsErrOpenIDConnectInitialize(err) {
|
||||||
|
|
|
@ -211,6 +211,14 @@
|
||||||
<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
|
<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Source.IsLDAP}}
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label><strong>{{.i18n.Tr "admin.auths.syncenabled"}}</strong></label>
|
||||||
|
<input name="is_sync_enabled" type="checkbox" {{if .Source.IsSyncEnabled}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
|
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
|
||||||
|
|
|
@ -61,6 +61,12 @@
|
||||||
<input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}>
|
<input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label><strong>{{.i18n.Tr "admin.auths.syncenabled"}}</strong></label>
|
||||||
|
<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
|
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
|
||||||
|
|
|
@ -45,6 +45,10 @@
|
||||||
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
|
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
|
||||||
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=7">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=7">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td>
|
||||||
|
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=8">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue