| @@ -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 | |||
| @@ -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) | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -28,6 +28,7 @@ type AuthenticationForm struct { | |||
| Filter string | |||
| AdminFilter string | |||
| IsActive bool | |||
| IsSyncEnabled bool | |||
| SMTPAuth string | |||
| SMTPHost string | |||
| SMTPPort int | |||
| @@ -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 | |||
| } | |||
| @@ -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() | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 { | |||
| @@ -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) { | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||