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.
release/v1.15
Adam Strzelecki 2015-12-01 14:49:49 +01:00
parent 7ccce4d110
commit 573305f3d3
7 changed files with 97 additions and 67 deletions

View File

@ -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.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_base = User Search Base
auths.user_dn = User DN 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_name = First name attribute
auths.attribute_surname = Surname attribute auths.attribute_surname = Surname attribute
auths.attribute_mail = E-mail attribute auths.attribute_mail = E-mail attribute

View File

@ -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. // and create a local user if success when enabled.
// It returns the same LoginUserPlain semantic. // 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) cfg := source.Cfg.(*LDAPConfig)
directBind := (source.Type == DLDAP) 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 { if !logged {
// User not in LDAP, do nothing // User not in LDAP, do nothing
return nil, ErrUserNotExist{0, name} return nil, ErrUserNotExist{0, loginName}
} }
if !autoRegister { if !autoRegister {
@ -242,6 +242,9 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto
} }
// Fallback. // Fallback.
if len(name) == 0 {
name = loginName
}
if len(mail) == 0 { if len(mail) == 0 {
mail = fmt.Sprintf("%s@localhost", name) mail = fmt.Sprintf("%s@localhost", name)
} }
@ -249,10 +252,10 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto
u = &User{ u = &User{
LowerName: strings.ToLower(name), LowerName: strings.ToLower(name),
Name: name, Name: name,
FullName: strings.TrimSpace(fn + " " + sn), FullName: composeFullName(fn, sn, name),
LoginType: source.Type, LoginType: source.Type,
LoginSource: source.ID, LoginSource: source.ID,
LoginName: name, LoginName: loginName,
Email: mail, Email: mail,
IsAdmin: admin, IsAdmin: admin,
IsActive: true, IsActive: true,
@ -260,6 +263,19 @@ func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, auto
return u, CreateUser(u) 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
}
}
// _________ __________________________ // _________ __________________________
// / _____/ / \__ ___/\______ \ // / _____/ / \__ ___/\______ \
// \_____ \ / \ / \| | | ___/ // \_____ \ / \ / \| | | ___/

View File

@ -10,28 +10,29 @@ import (
) )
type AuthenticationForm struct { type AuthenticationForm struct {
ID int64 ID int64
Type int `binding:"Range(2,5)"` Type int `binding:"Range(2,5)"`
Name string `binding:"Required;MaxSize(30)"` Name string `binding:"Required;MaxSize(30)"`
Host string Host string
Port int Port int
BindDN string BindDN string
BindPassword string BindPassword string
UserBase string UserBase string
UserDN string `form:"user_dn"` UserDN string `form:"user_dn"`
AttributeName string AttributeUsername string
AttributeSurname string AttributeName string
AttributeMail string AttributeSurname string
Filter string AttributeMail string
AdminFilter string Filter string
IsActive bool AdminFilter string
SMTPAuth string IsActive bool
SMTPHost string SMTPAuth string
SMTPPort int SMTPHost string
AllowedDomains string SMTPPort int
TLS bool AllowedDomains string
SkipVerify bool TLS bool
PAMServiceName string `form:"pam_service_name"` SkipVerify bool
PAMServiceName string `form:"pam_service_name"`
} }
func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View File

@ -18,21 +18,22 @@ import (
// Basic LDAP authentication service // Basic LDAP authentication service
type Source struct { type Source struct {
Name string // canonical name (ie. corporate.ad) Name string // canonical name (ie. corporate.ad)
Host string // LDAP host Host string // LDAP host
Port int // port number Port int // port number
UseSSL bool // Use SSL UseSSL bool // Use SSL
SkipVerify bool SkipVerify bool
BindDN string // DN to bind with BindDN string // DN to bind with
BindPassword string // Bind DN password BindPassword string // Bind DN password
UserBase string // Base search path for users UserBase string // Base search path for users
UserDN string // Template for the DN of the user for simple auth UserDN string // Template for the DN of the user for simple auth
AttributeName string // First name attribute AttributeUsername string // Username attribute
AttributeSurname string // Surname attribute AttributeName string // First name attribute
AttributeMail string // E-mail attribute AttributeSurname string // Surname attribute
Filter string // Query filter to validate entry AttributeMail string // E-mail attribute
AdminFilter string // Query filter to check if user is admin Filter string // Query filter to validate entry
Enabled bool // if this source is disabled 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) { 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 // 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 var userDN string
if directBind { if directBind {
log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) 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 var ok bool
userDN, ok = ls.sanitizedUserDN(name) userDN, ok = ls.sanitizedUserDN(name)
if !ok { if !ok {
return "", "", "", false, false return "", "", "", "", false, false
} }
} else { } else {
log.Trace("LDAP will use BindDN.") log.Trace("LDAP will use BindDN.")
@ -125,7 +126,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var found bool var found bool
userDN, found = ls.FindUserDN(name) userDN, found = ls.FindUserDN(name)
if !found { 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 { 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 "", "", "", "", false, false
} }
defer l.Close() defer l.Close()
@ -141,13 +142,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
err = l.Bind(userDN, passwd) err = l.Bind(userDN, passwd)
if err != nil { if err != nil {
log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err) 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) log.Trace("Bound successfully with userDN: %s", userDN)
userFilter, ok := ls.sanitizedUserQuery(name) userFilter, ok := ls.sanitizedUserQuery(name)
if !ok { if !ok {
return "", "", "", false, false return "", "", "", "", false, false
} }
search := ldap.NewSearchRequest( search := ldap.NewSearchRequest(
@ -158,7 +159,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 "", "", "", "", false, false
} 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.")
@ -166,9 +167,10 @@ 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 "", "", "", "", false, false
} }
username_attr := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
name_attr := sr.Entries[0].GetAttributeValue(ls.AttributeName) name_attr := sr.Entries[0].GetAttributeValue(ls.AttributeName)
sn_attr := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) sn_attr := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
mail_attr := sr.Entries[0].GetAttributeValue(ls.AttributeMail) 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) { func ldapDial(ls *Source) (*ldap.Conn, error) {

View File

@ -68,21 +68,22 @@ func NewAuthSource(ctx *middleware.Context) {
func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig { func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
return &models.LDAPConfig{ return &models.LDAPConfig{
Source: &ldap.Source{ Source: &ldap.Source{
Name: form.Name, Name: form.Name,
Host: form.Host, Host: form.Host,
Port: form.Port, Port: form.Port,
UseSSL: form.TLS, UseSSL: form.TLS,
SkipVerify: form.SkipVerify, SkipVerify: form.SkipVerify,
BindDN: form.BindDN, BindDN: form.BindDN,
UserDN: form.UserDN, UserDN: form.UserDN,
BindPassword: form.BindPassword, BindPassword: form.BindPassword,
UserBase: form.UserBase, UserBase: form.UserBase,
AttributeName: form.AttributeName, AttributeUsername: form.AttributeUsername,
AttributeSurname: form.AttributeSurname, AttributeName: form.AttributeName,
AttributeMail: form.AttributeMail, AttributeSurname: form.AttributeSurname,
Filter: form.Filter, AttributeMail: form.AttributeMail,
AdminFilter: form.AdminFilter, Filter: form.Filter,
Enabled: true, AdminFilter: form.AdminFilter,
Enabled: true,
}, },
} }
} }

View File

@ -63,6 +63,10 @@
<label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label> <label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label>
<input id="admin_filter" name="admin_filter" value="{{$cfg.AdminFilter}}"> <input id="admin_filter" name="admin_filter" value="{{$cfg.AdminFilter}}">
</div> </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"> <div class="field">
<label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label> <label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label>
<input id="attribute_name" name="attribute_name" value="{{$cfg.AttributeName}}"> <input id="attribute_name" name="attribute_name" value="{{$cfg.AttributeName}}">

View File

@ -66,6 +66,10 @@
<label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label> <label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label>
<input id="admin_filter" name="admin_filter" value="{{.admin_filter}}"> <input id="admin_filter" name="admin_filter" value="{{.admin_filter}}">
</div> </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"> <div class="field">
<label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label> <label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label>
<input id="attribute_name" name="attribute_name" value="{{.attribute_name}}"> <input id="attribute_name" name="attribute_name" value="{{.attribute_name}}">