瀏覽代碼

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.
Adam Strzelecki 9 年之前
父節點
當前提交
573305f3d3

+ 2 - 0
conf/locale/locale_en-US.ini

@@ -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

+ 22 - 6
models/login.go

@@ -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
+	}
+}
+
 //   _________   __________________________
 //  /   _____/  /     \__    ___/\______   \
 //  \_____  \  /  \ /  \|    |    |     ___/

+ 23 - 22
modules/auth/auth_form.go

@@ -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 {

+ 26 - 24
modules/auth/ldap/ldap.go

@@ -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) {

+ 16 - 15
routers/admin/auths.go

@@ -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,
 		},
 	}
 }

+ 4 - 0
templates/admin/auth/edit.tmpl

@@ -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}}">

+ 4 - 0
templates/admin/auth/new.tmpl

@@ -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}}">