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.
tags/v1.21.12.1
| @@ -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 | ||||
| @@ -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 | |||||
| } | |||||
| } | |||||
| // _________ __________________________ | // _________ __________________________ | ||||
| // / _____/ / \__ ___/\______ \ | // / _____/ / \__ ___/\______ \ | ||||
| // \_____ \ / \ / \| | | ___/ | // \_____ \ / \ / \| | | ___/ | ||||
| @@ -10,28 +10,29 @@ import ( | |||||
| ) | ) | ||||
| type AuthenticationForm struct { | 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 { | func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| @@ -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) | |||||
| 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) { | 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) { | ||||
| @@ -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, | |||||
| 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> | <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}}"> | ||||
| @@ -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}}"> | ||||