Skip to content

Commit 573305f

Browse files
committed
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.
1 parent 7ccce4d commit 573305f

File tree

7 files changed

+97
-67
lines changed

7 files changed

+97
-67
lines changed

conf/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,8 @@ auths.bind_password = Bind Password
878878
auths.bind_password_helper = Warning: This password is stored in plain text. Do not use a high privileged account.
879879
auths.user_base = User Search Base
880880
auths.user_dn = User DN
881+
auths.attribute_username = Username attribute
882+
auths.attribute_username_placeholder = Leave empty to use sign-in form field value for user name.
881883
auths.attribute_name = First name attribute
882884
auths.attribute_surname = Surname attribute
883885
auths.attribute_mail = E-mail attribute

models/login.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,41 +225,57 @@ func DeleteSource(source *LoginSource) error {
225225
// |_______ \/_______ /\____|__ /____|
226226
// \/ \/ \/
227227

228-
// LoginUserLDAPSource queries if name/passwd can login against the LDAP directory pool,
228+
// LoginUserLDAPSource queries if loginName/passwd can login against the LDAP directory pool,
229229
// and create a local user if success when enabled.
230230
// It returns the same LoginUserPlain semantic.
231-
func LoginUserLDAPSource(u *User, name, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
231+
func LoginUserLDAPSource(u *User, loginName, passwd string, source *LoginSource, autoRegister bool) (*User, error) {
232232
cfg := source.Cfg.(*LDAPConfig)
233233
directBind := (source.Type == DLDAP)
234-
fn, sn, mail, admin, logged := cfg.SearchEntry(name, passwd, directBind)
234+
name, fn, sn, mail, admin, logged := cfg.SearchEntry(loginName, passwd, directBind)
235235
if !logged {
236236
// User not in LDAP, do nothing
237-
return nil, ErrUserNotExist{0, name}
237+
return nil, ErrUserNotExist{0, loginName}
238238
}
239239

240240
if !autoRegister {
241241
return u, nil
242242
}
243243

244244
// Fallback.
245+
if len(name) == 0 {
246+
name = loginName
247+
}
245248
if len(mail) == 0 {
246249
mail = fmt.Sprintf("%s@localhost", name)
247250
}
248251

249252
u = &User{
250253
LowerName: strings.ToLower(name),
251254
Name: name,
252-
FullName: strings.TrimSpace(fn + " " + sn),
255+
FullName: composeFullName(fn, sn, name),
253256
LoginType: source.Type,
254257
LoginSource: source.ID,
255-
LoginName: name,
258+
LoginName: loginName,
256259
Email: mail,
257260
IsAdmin: admin,
258261
IsActive: true,
259262
}
260263
return u, CreateUser(u)
261264
}
262265

266+
func composeFullName(firstName, surename, userName string) string {
267+
switch {
268+
case len(firstName) == 0 && len(surename) == 0:
269+
return userName
270+
case len(firstName) == 0:
271+
return surename
272+
case len(surename) == 0:
273+
return firstName
274+
default:
275+
return firstName + " " + surename
276+
}
277+
}
278+
263279
// _________ __________________________
264280
// / _____/ / \__ ___/\______ \
265281
// \_____ \ / \ / \| | | ___/

modules/auth/auth_form.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ import (
1010
)
1111

1212
type AuthenticationForm struct {
13-
ID int64
14-
Type int `binding:"Range(2,5)"`
15-
Name string `binding:"Required;MaxSize(30)"`
16-
Host string
17-
Port int
18-
BindDN string
19-
BindPassword string
20-
UserBase string
21-
UserDN string `form:"user_dn"`
22-
AttributeName string
23-
AttributeSurname string
24-
AttributeMail string
25-
Filter string
26-
AdminFilter string
27-
IsActive bool
28-
SMTPAuth string
29-
SMTPHost string
30-
SMTPPort int
31-
AllowedDomains string
32-
TLS bool
33-
SkipVerify bool
34-
PAMServiceName string `form:"pam_service_name"`
13+
ID int64
14+
Type int `binding:"Range(2,5)"`
15+
Name string `binding:"Required;MaxSize(30)"`
16+
Host string
17+
Port int
18+
BindDN string
19+
BindPassword string
20+
UserBase string
21+
UserDN string `form:"user_dn"`
22+
AttributeUsername string
23+
AttributeName string
24+
AttributeSurname string
25+
AttributeMail string
26+
Filter string
27+
AdminFilter string
28+
IsActive bool
29+
SMTPAuth string
30+
SMTPHost string
31+
SMTPPort int
32+
AllowedDomains string
33+
TLS bool
34+
SkipVerify bool
35+
PAMServiceName string `form:"pam_service_name"`
3536
}
3637

3738
func (f *AuthenticationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

modules/auth/ldap/ldap.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@ import (
1818

1919
// Basic LDAP authentication service
2020
type Source struct {
21-
Name string // canonical name (ie. corporate.ad)
22-
Host string // LDAP host
23-
Port int // port number
24-
UseSSL bool // Use SSL
25-
SkipVerify bool
26-
BindDN string // DN to bind with
27-
BindPassword string // Bind DN password
28-
UserBase string // Base search path for users
29-
UserDN string // Template for the DN of the user for simple auth
30-
AttributeName string // First name attribute
31-
AttributeSurname string // Surname attribute
32-
AttributeMail string // E-mail attribute
33-
Filter string // Query filter to validate entry
34-
AdminFilter string // Query filter to check if user is admin
35-
Enabled bool // if this source is disabled
21+
Name string // canonical name (ie. corporate.ad)
22+
Host string // LDAP host
23+
Port int // port number
24+
UseSSL bool // Use SSL
25+
SkipVerify bool
26+
BindDN string // DN to bind with
27+
BindPassword string // Bind DN password
28+
UserBase string // Base search path for users
29+
UserDN string // Template for the DN of the user for simple auth
30+
AttributeUsername string // Username attribute
31+
AttributeName string // First name attribute
32+
AttributeSurname string // Surname attribute
33+
AttributeMail string // E-mail attribute
34+
Filter string // Query filter to validate entry
35+
AdminFilter string // Query filter to check if user is admin
36+
Enabled bool // if this source is disabled
3637
}
3738

3839
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -109,45 +110,45 @@ func (ls *Source) FindUserDN(name string) (string, bool) {
109110
}
110111

111112
// searchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
112-
func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, bool, bool) {
113+
func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
113114
var userDN string
114115
if directBind {
115116
log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
116117

117118
var ok bool
118119
userDN, ok = ls.sanitizedUserDN(name)
119120
if !ok {
120-
return "", "", "", false, false
121+
return "", "", "", "", false, false
121122
}
122123
} else {
123124
log.Trace("LDAP will use BindDN.")
124125

125126
var found bool
126127
userDN, found = ls.FindUserDN(name)
127128
if !found {
128-
return "", "", "", false, false
129+
return "", "", "", "", false, false
129130
}
130131
}
131132

132133
l, err := ldapDial(ls)
133134
if err != nil {
134135
log.Error(4, "LDAP Connect error (%s): %v", ls.Host, err)
135136
ls.Enabled = false
136-
return "", "", "", false, false
137+
return "", "", "", "", false, false
137138
}
138139
defer l.Close()
139140

140141
log.Trace("Binding with userDN: %s", userDN)
141142
err = l.Bind(userDN, passwd)
142143
if err != nil {
143144
log.Debug("LDAP auth. failed for %s, reason: %v", userDN, err)
144-
return "", "", "", false, false
145+
return "", "", "", "", false, false
145146
}
146147

147148
log.Trace("Bound successfully with userDN: %s", userDN)
148149
userFilter, ok := ls.sanitizedUserQuery(name)
149150
if !ok {
150-
return "", "", "", false, false
151+
return "", "", "", "", false, false
151152
}
152153

153154
search := ldap.NewSearchRequest(
@@ -158,17 +159,18 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
158159
sr, err := l.Search(search)
159160
if err != nil {
160161
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
161-
return "", "", "", false, false
162+
return "", "", "", "", false, false
162163
} else if len(sr.Entries) < 1 {
163164
if directBind {
164165
log.Error(4, "User filter inhibited user login.")
165166
} else {
166167
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
167168
}
168169

169-
return "", "", "", false, false
170+
return "", "", "", "", false, false
170171
}
171172

173+
username_attr := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
172174
name_attr := sr.Entries[0].GetAttributeValue(ls.AttributeName)
173175
sn_attr := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
174176
mail_attr := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
@@ -190,7 +192,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
190192
}
191193
}
192194

193-
return name_attr, sn_attr, mail_attr, admin_attr, true
195+
return username_attr, name_attr, sn_attr, mail_attr, admin_attr, true
194196
}
195197

196198
func ldapDial(ls *Source) (*ldap.Conn, error) {

routers/admin/auths.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,22 @@ func NewAuthSource(ctx *middleware.Context) {
6868
func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
6969
return &models.LDAPConfig{
7070
Source: &ldap.Source{
71-
Name: form.Name,
72-
Host: form.Host,
73-
Port: form.Port,
74-
UseSSL: form.TLS,
75-
SkipVerify: form.SkipVerify,
76-
BindDN: form.BindDN,
77-
UserDN: form.UserDN,
78-
BindPassword: form.BindPassword,
79-
UserBase: form.UserBase,
80-
AttributeName: form.AttributeName,
81-
AttributeSurname: form.AttributeSurname,
82-
AttributeMail: form.AttributeMail,
83-
Filter: form.Filter,
84-
AdminFilter: form.AdminFilter,
85-
Enabled: true,
71+
Name: form.Name,
72+
Host: form.Host,
73+
Port: form.Port,
74+
UseSSL: form.TLS,
75+
SkipVerify: form.SkipVerify,
76+
BindDN: form.BindDN,
77+
UserDN: form.UserDN,
78+
BindPassword: form.BindPassword,
79+
UserBase: form.UserBase,
80+
AttributeUsername: form.AttributeUsername,
81+
AttributeName: form.AttributeName,
82+
AttributeSurname: form.AttributeSurname,
83+
AttributeMail: form.AttributeMail,
84+
Filter: form.Filter,
85+
AdminFilter: form.AdminFilter,
86+
Enabled: true,
8687
},
8788
}
8889
}

templates/admin/auth/edit.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
<label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label>
6464
<input id="admin_filter" name="admin_filter" value="{{$cfg.AdminFilter}}">
6565
</div>
66+
<div class="field">
67+
<label for="attribute_username">{{.i18n.Tr "admin.auths.attribute_username"}}</label>
68+
<input id="attribute_username" name="attribute_username" value="{{$cfg.AttributeUsername}}" placeholder="{{.i18n.Tr "admin.auths.attribute_username_placeholder"}}">
69+
</div>
6670
<div class="field">
6771
<label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label>
6872
<input id="attribute_name" name="attribute_name" value="{{$cfg.AttributeName}}">

templates/admin/auth/new.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
<label for="admin_filter">{{.i18n.Tr "admin.auths.admin_filter"}}</label>
6767
<input id="admin_filter" name="admin_filter" value="{{.admin_filter}}">
6868
</div>
69+
<div class="field">
70+
<label for="attribute_username">{{.i18n.Tr "admin.auths.attribute_username"}}</label>
71+
<input id="attribute_username" name="attribute_username" value="{{.attribute_username}}" placeholder="{{.i18n.Tr "admin.auths.attribute_username_placeholder"}}">
72+
</div>
6973
<div class="field">
7074
<label for="attribute_name">{{.i18n.Tr "admin.auths.attribute_name"}}</label>
7175
<input id="attribute_name" name="attribute_name" value="{{.attribute_name}}">

0 commit comments

Comments
 (0)