| @@ -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 | ||||
| @@ -140,11 +140,12 @@ func (cfg *OAuth2Config) ToDB() ([]byte, error) { | |||||
| // LoginSource represents an external way for authorizing users. | // LoginSource represents an external way for authorizing users. | ||||
| type LoginSource struct { | 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:"-"` | Created time.Time `xorm:"-"` | ||||
| CreatedUnix int64 `xorm:"INDEX"` | CreatedUnix int64 `xorm:"INDEX"` | ||||
| @@ -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) | |||||
| if !succeed { | |||||
| sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP) | |||||
| 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 { | |||||
| username = login | |||||
| if len(sr.Username) == 0 { | |||||
| sr.Username = login | |||||
| } | } | ||||
| // Validate username make sure it satisfies requirement. | // 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{ | 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, | 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 | ||||
| @@ -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 | 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 !directBind && ls.AttributesInBind { | |||||
| // binds user (checking password) after looking-up attributes in BindDN context | |||||
| err = bindUser(l, userDN, passwd) | |||||
| if err != nil { | 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 { | 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() | 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 | ||||
| @@ -186,10 +187,11 @@ func NewAuthSourcePost(ctx *context.Context, form auth.AuthenticationForm) { | |||||
| } | } | ||||
| if err := models.CreateLoginSource(&models.LoginSource{ | 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 { | }); err != nil { | ||||
| if models.IsErrLoginSourceAlreadyExist(err) { | if models.IsErrLoginSourceAlreadyExist(err) { | ||||
| ctx.Data["Err_Name"] = true | ctx.Data["Err_Name"] = true | ||||
| @@ -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> | ||||