mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	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:
		@@ -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}}">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user