LDAP user synchronization (#1478)

This commit is contained in:
Lauris BH 2017-05-10 16:10:18 +03:00 committed by Kim "BKC" Carlbäcker
parent fd76f090a2
commit 524885dd65
15 changed files with 356 additions and 52 deletions

10
conf/app.ini vendored
View file

@ -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

View file

@ -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)
} }

View file

@ -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
View 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
}

View file

@ -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)
}
}
}
}
}
}
}

View file

@ -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

View file

@ -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
} }

View file

@ -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()
} }

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -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>