summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--conf/app.ini10
-rw-r--r--models/login_source.go41
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v31.go35
-rw-r--r--models/user.go127
-rw-r--r--modules/auth/auth_form.go1
-rw-r--r--modules/auth/ldap/ldap.go125
-rw-r--r--modules/cron/cron.go11
-rw-r--r--modules/setting/setting.go17
-rw-r--r--options/locale/locale_en-US.ini4
-rw-r--r--routers/admin/admin.go4
-rw-r--r--routers/admin/auths.go11
-rw-r--r--templates/admin/auth/edit.tmpl8
-rw-r--r--templates/admin/auth/new.tmpl6
-rw-r--r--templates/admin/dashboard.tmpl4
15 files changed, 355 insertions, 51 deletions
diff --git a/conf/app.ini b/conf/app.ini
index 47fd4b1182..4f7dc9946b 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -442,6 +442,16 @@ SCHEDULE = @every 24h
; Archives created more than OLDER_THAN ago are subject to deletion
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]
; Disables highlight of added and removed changes
DISABLE_DIFF_HIGHLIGHT = false
diff --git a/models/login_source.go b/models/login_source.go
index 3c7bff8cb8..60110708cb 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -140,11 +140,12 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) {
// LoginSource represents an external way for authorizing users.
type LoginSource struct {
- ID int64 `xorm:"pk autoincr"`
- Type LoginType
- Name string `xorm:"UNIQUE"`
- IsActived bool `xorm:"INDEX NOT NULL DEFAULT false"`
- Cfg core.Conversion `xorm:"TEXT"`
+ ID int64 `xorm:"pk autoincr"`
+ Type LoginType
+ 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"`
@@ -294,6 +295,10 @@ func CreateLoginSource(source *LoginSource) error {
} else if has {
return ErrLoginSourceAlreadyExist{source.Name}
}
+ // Synchronization is only aviable with LDAP for now
+ if !source.IsLDAP() {
+ source.IsSyncEnabled = false
+ }
_, err = x.Insert(source)
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,
// and create a local user if success when enabled.
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)
- if !succeed {
+ sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
+ if sr == nil {
// User not in LDAP, do nothing
return nil, ErrUserNotExist{0, login, 0}
}
@@ -416,28 +421,28 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoR
}
// Fallback.
- if len(username) == 0 {
- username = login
+ if len(sr.Username) == 0 {
+ sr.Username = login
}
// Validate username make sure it satisfies requirement.
- if binding.AlphaDashDotPattern.MatchString(username) {
- return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", 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", sr.Username)
}
- if len(mail) == 0 {
- mail = fmt.Sprintf("%s@localhost", username)
+ if len(sr.Mail) == 0 {
+ sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
}
user = &User{
- LowerName: strings.ToLower(username),
- Name: username,
- FullName: composeFullName(fn, sn, username),
- Email: mail,
+ LowerName: strings.ToLower(sr.Username),
+ Name: sr.Username,
+ FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
+ Email: sr.Mail,
LoginType: source.Type,
LoginSource: source.ID,
LoginName: login,
IsActive: true,
- IsAdmin: isAdmin,
+ IsAdmin: sr.IsAdmin,
}
return user, CreateUser(user)
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 4877a9fb02..000412ae37 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -110,6 +110,8 @@ var migrations = []Migration{
NewMigration("add commit status table", addCommitStatus),
// v30 -> 31
NewMigration("add primary key to external login user", addExternalLoginUserPK),
+ // 31 -> 32
+ NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
}
// Migrate database to current version
diff --git a/models/migrations/v31.go b/models/migrations/v31.go
new file mode 100644
index 0000000000..1166a5f6c4
--- /dev/null
+++ b/models/migrations/v31.go
@@ -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
+}
diff --git a/models/user.go b/models/user.go
index e95bf5cd44..7e6bbd5dc3 100644
--- a/models/user.go
+++ b/models/user.go
@@ -50,6 +50,8 @@ const (
UserTypeOrganization
)
+const syncExternalUsers = "sync_external_users"
+
var (
// ErrUserNotKeyOwner user does not own this key error
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
}
+
+// 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)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/modules/auth/auth_form.go b/modules/auth/auth_form.go
index 8dc039835f..7c452bbc35 100644
--- a/modules/auth/auth_form.go
+++ b/modules/auth/auth_form.go
@@ -28,6 +28,7 @@ type AuthenticationForm struct {
Filter string
AdminFilter string
IsActive bool
+ IsSyncEnabled bool
SMTPAuth string
SMTPHost string
SMTPPort int
diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go
index 3064b31958..7754cc8182 100644
--- a/modules/auth/ldap/ldap.go
+++ b/modules/auth/ldap/ldap.go
@@ -47,6 +47,15 @@ type Source struct {
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) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
@@ -149,18 +158,39 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
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
-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
if len(passwd) == 0 {
log.Debug("Auth. failed for %s, password cannot be empty")
- return "", "", "", "", false, false
+ return nil
}
l, err := dial(ls)
if err != nil {
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
ls.Enabled = false
- return "", "", "", "", false, false
+ return nil
}
defer l.Close()
@@ -171,7 +201,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var ok bool
userDN, ok = ls.sanitizedUserDN(name)
if !ok {
- return "", "", "", "", false, false
+ return nil
}
} else {
log.Trace("LDAP will use BindDN.")
@@ -179,7 +209,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var found bool
userDN, found = ls.findUserDN(l, name)
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
err = bindUser(l, userDN, passwd)
if err != nil {
- return "", "", "", "", false, false
+ return nil
}
}
userFilter, ok := ls.sanitizedUserQuery(name)
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)
@@ -205,7 +235,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
sr, err := l.Search(search)
if err != nil {
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
- return "", "", "", "", false, false
+ return nil
} else if len(sr.Entries) < 1 {
if directBind {
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)")
}
- return "", "", "", "", false, false
+ return nil
}
username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
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 !directBind && ls.AttributesInBind {
+ // binds user (checking password) after looking-up attributes in BindDN context
+ err = bindUser(l, userDN, passwd)
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
+ return nil
}
}
- if !directBind && ls.AttributesInBind {
- // binds user (checking password) after looking-up attributes in BindDN context
- err = bindUser(l, userDN, passwd)
+ 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 {
- return "", "", "", "", false, false
+ 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 username, firstname, surname, mail, isAdmin, true
+ return result
}
diff --git a/modules/cron/cron.go b/modules/cron/cron.go
index 785bf44ada..a64b51253c 100644
--- a/modules/cron/cron.go
+++ b/modules/cron/cron.go
@@ -66,6 +66,17 @@ func NewContext() {
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()
}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index c3ed4ef971..4acad42393 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -336,6 +336,12 @@ var (
Schedule string
OlderThan time.Duration
} `ini:"cron.archive_cleanup"`
+ SyncExternalUsers struct {
+ Enabled bool
+ RunAtStart bool
+ Schedule string
+ UpdateExisting bool
+ } `ini:"cron.sync_external_users"`
}{
UpdateMirror: struct {
Enabled bool
@@ -379,6 +385,17 @@ var (
Schedule: "@every 24h",
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
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fadb90a9e3..cd24ec9349 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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.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.sync_external_users = Synchronize external user data
+dashboard.sync_external_users_started = External user synchronization started
dashboard.server_uptime = Server Uptime
dashboard.current_goroutine = Current Goroutines
dashboard.current_memory_usage = Current Memory Usage
@@ -1147,6 +1148,7 @@ auths.new = Add New Source
auths.name = Name
auths.type = Type
auths.enabled = Enabled
+auths.syncenabled = Enable user synchronization
auths.updated = Updated
auths.auth_type = Authentication Type
auths.auth_name = Authentication Name
diff --git a/routers/admin/admin.go b/routers/admin/admin.go
index 6b5b33f734..8ae4504847 100644
--- a/routers/admin/admin.go
+++ b/routers/admin/admin.go
@@ -121,6 +121,7 @@ const (
syncSSHAuthorizedKey
syncRepositoryUpdateHook
reinitMissingRepository
+ syncExternalUsers
)
// Dashboard show admin panel dashboard
@@ -157,6 +158,9 @@ func Dashboard(ctx *context.Context) {
case reinitMissingRepository:
success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
err = models.ReinitMissingRepositories()
+ case syncExternalUsers:
+ success = ctx.Tr("admin.dashboard.sync_external_users_started")
+ go models.SyncExternalUsers()
}
if err != nil {
diff --git a/routers/admin/auths.go b/routers/admin/auths.go
index eb7c7e8e93..590e45a4f4 100644
--- a/routers/admin/auths.go
+++ b/routers/admin/auths.go
@@ -74,6 +74,7 @@ func NewAuthSource(ctx *context.Context) {
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
ctx.Data["smtp_auth"] = "PLAIN"
ctx.Data["is_active"] = true
+ ctx.Data["is_sync_enabled"] = true
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = models.SMTPAuths
@@ -186,10 +187,11 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
}
if err := models.CreateLoginSource(&models.LoginSource{
- Type: models.LoginType(form.Type),
- Name: form.Name,
- IsActived: form.IsActive,
- Cfg: config,
+ Type: models.LoginType(form.Type),
+ Name: form.Name,
+ IsActived: form.IsActive,
+ IsSyncEnabled: form.IsSyncEnabled,
+ Cfg: config,
}); err != nil {
if models.IsErrLoginSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
@@ -273,6 +275,7 @@ func EditAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) {
source.Name = form.Name
source.IsActived = form.IsActive
+ source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
if err := models.UpdateSource(source); err != nil {
if models.IsErrOpenIDConnectInitialize(err) {
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 3c74b2ad17..e3048b2183 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -211,6 +211,14 @@
<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
</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="ui checkbox">
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index 00239b0462..46db82c3a7 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -61,6 +61,12 @@
<input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}>
</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="ui checkbox">
<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl
index 229cd305b9..23fc4a422d 100644
--- a/templates/admin/dashboard.tmpl
+++ b/templates/admin/dashboard.tmpl
@@ -45,6 +45,10 @@
<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>
</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>
</table>
</div>