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.master
@@ -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, | |||
}, | |||
} | |||
} | |||
@@ -64,6 +64,10 @@ | |||
<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}}"> | |||
</div> | |||
@@ -67,6 +67,10 @@ | |||
<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}}"> | |||
</div> | |||