LDAP: Optional user name attribute specification
Consider following LDAP search query example:
    (&(objectClass=Person)(|(uid=%s)(mail=%s)))
Right now on first login attempt Gogs will use the text supplied on login form
as the newly created user name. In example query above the text matches against
both e-mail or user name. So if user puts the e-mail then the new Gogs user
name will be e-mail which may be undesired.
Using optional user name attribute setting we can explicitly say we want Gogs
user name to be certain LDAP attribute eg. `uid`, so even user will use e-mail
to login 1st time, the new account will receive correct user name.
			
			
This commit is contained in:
		
							parent
							
								
									7ccce4d110
								
							
						
					
					
						commit
						573305f3d3
					
				
					 7 changed files with 97 additions and 67 deletions
				
			
		|  | @ -878,6 +878,8 @@ auths.bind_password = Bind Password | |||
| auths.bind_password_helper = Warning: This password is stored in plain text. Do not use a high privileged account. | ||||
| auths.user_base = User Search Base | ||||
| auths.user_dn = User DN | ||||
| auths.attribute_username = Username attribute | ||||
| auths.attribute_username_placeholder = Leave empty to use sign-in form field value for user name. | ||||
| auths.attribute_name = First name attribute | ||||
| auths.attribute_surname = Surname attribute | ||||
| auths.attribute_mail = E-mail attribute | ||||
|  |  | |||
|  | @ -225,16 +225,16 @@ func DeleteSource(source *LoginSource) error { | |||
| // |_______ \/_______  /\____|__  /____|
 | ||||
| //         \/        \/         \/
 | ||||
| 
 | ||||
| // LoginUserLDAPSource queries if name/passwd can login against the LDAP directory pool,
 | ||||
| // LoginUserLDAPSource queries if loginName/passwd can login against the LDAP directory pool,
 | ||||
| // and create a local user if success when enabled.
 | ||||
| // It returns the same LoginUserPlain semantic.
 | ||||
| func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) { | ||||
| func LoginUserLDAPSource(u *User, loginName, passwd string, source *LoginSource, autoRegister bool) (*User, error) { | ||||
| 	cfg := source.Cfg.(*LDAPConfig) | ||||
| 	directBind := (source.Type == DLDAP) | ||||
| 	fn, sn, mail, admin, logged := cfg.SearchEntry(name, passwd, directBind) | ||||
| 	name, fn, sn, mail, admin, logged := cfg.SearchEntry(loginName, passwd, directBind) | ||||
| 	if !logged { | ||||
| 		// User not in LDAP, do nothing
 | ||||
| 		return nil, ErrUserNotExist{0, name} | ||||
| 		return nil, ErrUserNotExist{0, loginName} | ||||
| 	} | ||||
| 
 | ||||
| 	if !autoRegister { | ||||
|  | @ -242,6 +242,9 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto | |||
| 	} | ||||
| 
 | ||||
| 	// Fallback.
 | ||||
| 	if len(name) == 0 { | ||||
| 		name = loginName | ||||
| 	} | ||||
| 	if len(mail) == 0 { | ||||
| 		mail = fmt.Sprintf("%s@localhost", name) | ||||
| 	} | ||||
|  | @ -249,10 +252,10 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto | |||
| 	u = &User{ | ||||
| 		LowerName:   strings.ToLower(name), | ||||
| 		Name:        name, | ||||
| 		FullName:    strings.TrimSpace(fn + " " + sn), | ||||
| 		FullName:    composeFullName(fn, sn, name), | ||||
| 		LoginType:   source.Type, | ||||
| 		LoginSource: source.ID, | ||||
| 		LoginName:   name, | ||||
| 		LoginName:   loginName, | ||||
| 		Email:       mail, | ||||
| 		IsAdmin:     admin, | ||||
| 		IsActive:    true, | ||||
|  | @ -260,6 +263,19 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto | |||
| 	return u, CreateUser(u) | ||||
| } | ||||
| 
 | ||||
| func composeFullName(firstName, surename, userName string) string { | ||||
| 	switch { | ||||
| 	case len(firstName) == 0 && len(surename) == 0: | ||||
| 		return userName | ||||
| 	case len(firstName) == 0: | ||||
| 		return surename | ||||
| 	case len(surename) == 0: | ||||
| 		return firstName | ||||
| 	default: | ||||
| 		return firstName + " " + surename | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| //   _________   __________________________
 | ||||
| //  /   _____/  /     \__    ___/\______   \
 | ||||
| //  \_____  \  /  \ /  \|    |    |     ___/
 | ||||
|  |  | |||
|  | @ -10,28 +10,29 @@ import ( | |||
| ) | ||||
| 
 | ||||
| type AuthenticationForm struct { | ||||
| 	ID               int64 | ||||
| 	Type             int    `binding:"Range(2,5)"` | ||||
| 	Name             string `binding:"Required;MaxSize(30)"` | ||||
| 	Host             string | ||||
| 	Port             int | ||||
| 	BindDN           string | ||||
| 	BindPassword     string | ||||
| 	UserBase         string | ||||
| 	UserDN           string `form:"user_dn"` | ||||
| 	AttributeName    string | ||||
| 	AttributeSurname string | ||||
| 	AttributeMail    string | ||||
| 	Filter           string | ||||
| 	AdminFilter      string | ||||
| 	IsActive         bool | ||||
| 	SMTPAuth         string | ||||
| 	SMTPHost         string | ||||
| 	SMTPPort         int | ||||
| 	AllowedDomains   string | ||||
| 	TLS              bool | ||||
| 	SkipVerify       bool | ||||
| 	PAMServiceName   string `form:"pam_service_name"` | ||||
| 	ID                int64 | ||||
| 	Type              int    `binding:"Range(2,5)"` | ||||
| 	Name              string `binding:"Required;MaxSize(30)"` | ||||
| 	Host              string | ||||
| 	Port              int | ||||
| 	BindDN            string | ||||
| 	BindPassword      string | ||||
| 	UserBase          string | ||||
| 	UserDN            string `form:"user_dn"` | ||||
| 	AttributeUsername string | ||||
| 	AttributeName     string | ||||
| 	AttributeSurname  string | ||||
| 	AttributeMail     string | ||||
| 	Filter            string | ||||
| 	AdminFilter       string | ||||
| 	IsActive          bool | ||||
| 	SMTPAuth          string | ||||
| 	SMTPHost          string | ||||
| 	SMTPPort          int | ||||
| 	AllowedDomains    string | ||||
| 	TLS               bool | ||||
| 	SkipVerify        bool | ||||
| 	PAMServiceName    string `form:"pam_service_name"` | ||||
| } | ||||
| 
 | ||||
| func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
|  |  | |||
|  | @ -18,21 +18,22 @@ import ( | |||
| 
 | ||||
| // Basic LDAP authentication service
 | ||||
| type Source struct { | ||||
| 	Name             string // canonical name (ie. corporate.ad)
 | ||||
| 	Host             string // LDAP host
 | ||||
| 	Port             int    // port number
 | ||||
| 	UseSSL           bool   // Use SSL
 | ||||
| 	SkipVerify       bool | ||||
| 	BindDN           string // DN to bind with
 | ||||
| 	BindPassword     string // Bind DN password
 | ||||
| 	UserBase         string // Base search path for users
 | ||||
| 	UserDN           string // Template for the DN of the user for simple auth
 | ||||
| 	AttributeName    string // First name attribute
 | ||||
| 	AttributeSurname string // Surname attribute
 | ||||
| 	AttributeMail    string // E-mail attribute
 | ||||
| 	Filter           string // Query filter to validate entry
 | ||||
| 	AdminFilter      string // Query filter to check if user is admin
 | ||||
| 	Enabled          bool   // if this source is disabled
 | ||||
| 	Name              string // canonical name (ie. corporate.ad)
 | ||||
| 	Host              string // LDAP host
 | ||||
| 	Port              int    // port number
 | ||||
| 	UseSSL            bool   // Use SSL
 | ||||
| 	SkipVerify        bool | ||||
| 	BindDN            string // DN to bind with
 | ||||
| 	BindPassword      string // Bind DN password
 | ||||
| 	UserBase          string // Base search path for users
 | ||||
| 	UserDN            string // Template for the DN of the user for simple auth
 | ||||
| 	AttributeUsername string // Username attribute
 | ||||
| 	AttributeName     string // First name attribute
 | ||||
| 	AttributeSurname  string // Surname attribute
 | ||||
| 	AttributeMail     string // E-mail attribute
 | ||||
| 	Filter            string // Query filter to validate entry
 | ||||
| 	AdminFilter       string // Query filter to check if user is admin
 | ||||
| 	Enabled           bool   // if this source is disabled
 | ||||
| } | ||||
| 
 | ||||
| func (ls *Source) sanitizedUserQuery(username string) (string, bool) { | ||||
|  | @ -109,7 +110,7 @@ func (ls *Source) FindUserDN(name string) (string, bool) { | |||
| } | ||||
| 
 | ||||
| // 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, bool, bool) { | ||||
| func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) { | ||||
| 	var userDN string | ||||
| 	if directBind { | ||||
| 		log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) | ||||
|  | @ -117,7 +118,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 "", "", "", "", false, false | ||||
| 		} | ||||
| 	} else { | ||||
| 		log.Trace("LDAP will use BindDN.") | ||||
|  | @ -125,7 +126,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str | |||
| 		var found bool | ||||
| 		userDN, found = ls.FindUserDN(name) | ||||
| 		if !found { | ||||
| 			return "", "", "", false, false | ||||
| 			return "", "", "", "", false, false | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -133,7 +134,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str | |||
| 	if err != nil { | ||||
| 		log.Error(4, "LDAP Connect error (%s): %v", ls.Host, err) | ||||
| 		ls.Enabled = false | ||||
| 		return "", "", "", false, false | ||||
| 		return "", "", "", "", false, false | ||||
| 	} | ||||
| 	defer l.Close() | ||||
| 
 | ||||
|  | @ -141,13 +142,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str | |||
| 	err = l.Bind(userDN, passwd) | ||||
| 	if err != nil { | ||||
| 		log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err) | ||||
| 		return "", "", "", false, false | ||||
| 		return "", "", "", "", false, false | ||||
| 	} | ||||
| 
 | ||||
| 	log.Trace("Bound successfully with userDN: %s", userDN) | ||||
| 	userFilter, ok := ls.sanitizedUserQuery(name) | ||||
| 	if !ok { | ||||
| 		return "", "", "", false, false | ||||
| 		return "", "", "", "", false, false | ||||
| 	} | ||||
| 
 | ||||
| 	search := ldap.NewSearchRequest( | ||||
|  | @ -158,7 +159,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 "", "", "", "", false, false | ||||
| 	} else if len(sr.Entries) < 1 { | ||||
| 		if directBind { | ||||
| 			log.Error(4, "User filter inhibited user login.") | ||||
|  | @ -166,9 +167,10 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str | |||
| 			log.Error(4, "LDAP Search failed unexpectedly! (0 entries)") | ||||
| 		} | ||||
| 
 | ||||
| 		return "", "", "", false, false | ||||
| 		return "", "", "", "", false, false | ||||
| 	} | ||||
| 
 | ||||
| 	username_attr := sr.Entries[0].GetAttributeValue(ls.AttributeUsername) | ||||
| 	name_attr := sr.Entries[0].GetAttributeValue(ls.AttributeName) | ||||
| 	sn_attr := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) | ||||
| 	mail_attr := sr.Entries[0].GetAttributeValue(ls.AttributeMail) | ||||
|  | @ -190,7 +192,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return name_attr, sn_attr, mail_attr, admin_attr, true | ||||
| 	return username_attr, name_attr, sn_attr, mail_attr, admin_attr, true | ||||
| } | ||||
| 
 | ||||
| func ldapDial(ls *Source) (*ldap.Conn, error) { | ||||
|  |  | |||
|  | @ -68,21 +68,22 @@ func NewAuthSource(ctx *middleware.Context) { | |||
| func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig { | ||||
| 	return &models.LDAPConfig{ | ||||
| 		Source: &ldap.Source{ | ||||
| 			Name:             form.Name, | ||||
| 			Host:             form.Host, | ||||
| 			Port:             form.Port, | ||||
| 			UseSSL:           form.TLS, | ||||
| 			SkipVerify:       form.SkipVerify, | ||||
| 			BindDN:           form.BindDN, | ||||
| 			UserDN:           form.UserDN, | ||||
| 			BindPassword:     form.BindPassword, | ||||
| 			UserBase:         form.UserBase, | ||||
| 			AttributeName:    form.AttributeName, | ||||
| 			AttributeSurname: form.AttributeSurname, | ||||
| 			AttributeMail:    form.AttributeMail, | ||||
| 			Filter:           form.Filter, | ||||
| 			AdminFilter:      form.AdminFilter, | ||||
| 			Enabled:          true, | ||||
| 			Name:              form.Name, | ||||
| 			Host:              form.Host, | ||||
| 			Port:              form.Port, | ||||
| 			UseSSL:            form.TLS, | ||||
| 			SkipVerify:        form.SkipVerify, | ||||
| 			BindDN:            form.BindDN, | ||||
| 			UserDN:            form.UserDN, | ||||
| 			BindPassword:      form.BindPassword, | ||||
| 			UserBase:          form.UserBase, | ||||
| 			AttributeUsername: form.AttributeUsername, | ||||
| 			AttributeName:     form.AttributeName, | ||||
| 			AttributeSurname:  form.AttributeSurname, | ||||
| 			AttributeMail:     form.AttributeMail, | ||||
| 			Filter:            form.Filter, | ||||
| 			AdminFilter:       form.AdminFilter, | ||||
| 			Enabled:           true, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -63,6 +63,10 @@ | |||
|               <label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label> | ||||
|               <input id="admin_filter" name="admin_filter" value="{{$cfg.AdminFilter}}"> | ||||
|             </div> | ||||
|             <div class="field"> | ||||
|               <label for="attribute_username">{{.i18n.Tr "admin.auths.attribute_username"}}</label> | ||||
|               <input id="attribute_username" name="attribute_username" value="{{$cfg.AttributeUsername}}" placeholder="{{.i18n.Tr "admin.auths.attribute_username_placeholder"}}"> | ||||
|             </div> | ||||
|             <div class="field"> | ||||
|               <label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label> | ||||
|               <input id="attribute_name" name="attribute_name" value="{{$cfg.AttributeName}}"> | ||||
|  |  | |||
|  | @ -66,6 +66,10 @@ | |||
|                   <label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label> | ||||
|                   <input id="admin_filter" name="admin_filter" value="{{.admin_filter}}"> | ||||
|                 </div> | ||||
|                 <div class="field"> | ||||
|                   <label for="attribute_username">{{.i18n.Tr "admin.auths.attribute_username"}}</label> | ||||
|                   <input id="attribute_username" name="attribute_username" value="{{.attribute_username}}" placeholder="{{.i18n.Tr "admin.auths.attribute_username_placeholder"}}"> | ||||
|                 </div> | ||||
|                 <div class="field"> | ||||
|                   <label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label> | ||||
|                   <input id="attribute_name" name="attribute_name" value="{{.attribute_name}}"> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue