mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	LDAP user synchronization (#1478)
This commit is contained in:
		
				
					committed by
					
						 Kim "BKC" Carlbäcker
						Kim "BKC" Carlbäcker
					
				
			
			
				
	
			
			
			
						parent
						
							fd76f090a2
						
					
				
				
					commit
					524885dd65
				
			
							
								
								
									
										10
									
								
								conf/app.ini
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								conf/app.ini
									
									
									
									
										vendored
									
									
								
							| @@ -442,6 +442,16 @@ SCHEDULE = @every 24h | ||||
| ; Archives created more than OLDER_THAN ago are subject to deletion | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										35
									
								
								models/migrations/v31.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								models/migrations/v31.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| // Copyright 2017 The Gogs Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-xorm/core" | ||||
| 	"github.com/go-xorm/xorm" | ||||
| ) | ||||
|  | ||||
| func addLoginSourceSyncEnabledColumn(x *xorm.Engine) error { | ||||
| 	// LoginSource see models/login_source.go | ||||
| 	type LoginSource struct { | ||||
| 		ID            int64 `xorm:"pk autoincr"` | ||||
| 		Type          int | ||||
| 		Name          string          `xorm:"UNIQUE"` | ||||
| 		IsActived     bool            `xorm:"INDEX NOT NULL DEFAULT false"` | ||||
| 		IsSyncEnabled bool            `xorm:"INDEX NOT NULL DEFAULT false"` | ||||
| 		Cfg           core.Conversion `xorm:"TEXT"` | ||||
|  | ||||
| 		Created     time.Time `xorm:"-"` | ||||
| 		CreatedUnix int64     `xorm:"INDEX"` | ||||
| 		Updated     time.Time `xorm:"-"` | ||||
| 		UpdatedUnix int64     `xorm:"INDEX"` | ||||
| 	} | ||||
|  | ||||
| 	if err := x.Sync2(new(LoginSource)); err != nil { | ||||
| 		return fmt.Errorf("Sync2: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										127
									
								
								models/user.go
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								models/user.go
									
									
									
									
									
								
							| @@ -50,6 +50,8 @@ const ( | ||||
| 	UserTypeOrganization | ||||
| ) | ||||
|  | ||||
| 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 := 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 | ||||
| 		} | ||||
| 	} | ||||
| 	isAdmin := checkAdmin(l, ls, userDN) | ||||
|  | ||||
| 	if !directBind && ls.AttributesInBind { | ||||
| 		// binds user (checking password) after looking-up attributes in BindDN context | ||||
| 		err = bindUser(l, userDN, passwd) | ||||
| 		if err != nil { | ||||
| 			return "", "", "", "", false, false | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return username, firstname, surname, mail, isAdmin, true | ||||
| 	return &SearchResult{ | ||||
| 		Username: username, | ||||
| 		Name:     firstname, | ||||
| 		Surname:  surname, | ||||
| 		Mail:     mail, | ||||
| 		IsAdmin:  isAdmin, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SearchEntries : search an LDAP source for all users matching userFilter | ||||
| func (ls *Source) SearchEntries() []*SearchResult { | ||||
| 	l, err := dial(ls) | ||||
| 	if err != nil { | ||||
| 		log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err) | ||||
| 		ls.Enabled = false | ||||
| 		return nil | ||||
| 	} | ||||
| 	defer l.Close() | ||||
|  | ||||
| 	if ls.BindDN != "" && ls.BindPassword != "" { | ||||
| 		err := l.Bind(ls.BindDN, ls.BindPassword) | ||||
| 		if err != nil { | ||||
| 			log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		log.Trace("Bound as BindDN %s", ls.BindDN) | ||||
| 	} else { | ||||
| 		log.Trace("Proceeding with anonymous LDAP search.") | ||||
| 	} | ||||
|  | ||||
| 	userFilter := fmt.Sprintf(ls.Filter, "*") | ||||
|  | ||||
| 	log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, ls.UserBase) | ||||
| 	search := ldap.NewSearchRequest( | ||||
| 		ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, | ||||
| 		[]string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}, | ||||
| 		nil) | ||||
|  | ||||
| 	sr, err := l.Search(search) | ||||
| 	if err != nil { | ||||
| 		log.Error(4, "LDAP Search failed unexpectedly! (%v)", err) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	result := make([]*SearchResult, len(sr.Entries)) | ||||
|  | ||||
| 	for i, v := range sr.Entries { | ||||
| 		result[i] = &SearchResult{ | ||||
| 			Username: v.GetAttributeValue(ls.AttributeUsername), | ||||
| 			Name:     v.GetAttributeValue(ls.AttributeName), | ||||
| 			Surname:  v.GetAttributeValue(ls.AttributeSurname), | ||||
| 			Mail:     v.GetAttributeValue(ls.AttributeMail), | ||||
| 			IsAdmin:  checkAdmin(l, ls, v.DN), | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|   | ||||
| @@ -66,6 +66,17 @@ func NewContext() { | ||||
| 			go models.DeleteOldRepositoryArchives() | ||||
| 		} | ||||
| 	} | ||||
| 	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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user