Skip to content

Commit c3e8c94

Browse files
jolheiserzeripath
andauthored
Add check for LDAP group membership (#10869)
This is a port of gogs/gogs#4398 The only changes made by myself are: Add locales Add some JS to the UI Otherwise all code credit goes to @aboron Resolves #10829 Signed-off-by: jolheiser <[email protected]> Co-authored-by: zeripath <[email protected]>
1 parent 4c42fce commit c3e8c94

File tree

8 files changed

+173
-2
lines changed

8 files changed

+173
-2
lines changed

modules/auth/auth_form.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ type AuthenticationForm struct {
3030
SearchPageSize int
3131
Filter string
3232
AdminFilter string
33+
GroupsEnabled bool
34+
GroupDN string
35+
GroupFilter string
36+
GroupMemberUID string
37+
UserUID string
3338
RestrictedFilter string
3439
AllowDeactivateAll bool
3540
IsActive bool

modules/auth/ldap/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,21 @@ share the following fields:
103103
matching parameter will be substituted with the user's username.
104104
* Example: (&(objectClass=posixAccount)(cn=%s))
105105
* Example: (&(objectClass=posixAccount)(uid=%s))
106+
107+
**Verify group membership in LDAP** uses the following fields:
108+
109+
* Group Search Base (optional)
110+
* The LDAP DN used for groups.
111+
* Example: ou=group,dc=mydomain,dc=com
112+
113+
* Group Name Filter (optional)
114+
* An LDAP filter declaring how to find valid groups in the above DN.
115+
* Example: (|(cn=gitea_users)(cn=admins))
116+
117+
* User Attribute in Group (optional)
118+
* Which user LDAP attribute is listed in the group.
119+
* Example: uid
120+
121+
* Group Attribute for User (optional)
122+
* Which group LDAP attribute contains an array above user attribute names.
123+
* Example: memberUid

modules/auth/ldap/ldap.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2020 The Gitea Authors. All rights reserved.
23
// Use of this source code is governed by a MIT-style
34
// license that can be found in the LICENSE file.
45

@@ -13,7 +14,7 @@ import (
1314

1415
"code.gitea.io/gitea/modules/log"
1516

16-
ldap "gopkg.in/ldap.v3"
17+
"gopkg.in/ldap.v3"
1718
)
1819

1920
// SecurityProtocol protocol type
@@ -49,6 +50,11 @@ type Source struct {
4950
RestrictedFilter string // Query filter to check if user is restricted
5051
Enabled bool // if this source is disabled
5152
AllowDeactivateAll bool // Allow an empty search response to deactivate all users from this source
53+
GroupsEnabled bool // if the group checking is enabled
54+
GroupDN string // Group Search Base
55+
GroupFilter string // Group Name Filter
56+
GroupMemberUID string // Group Attribute containing array of UserUID
57+
UserUID string // User Attribute listed in Group
5258
}
5359

5460
// SearchResult : user data
@@ -84,6 +90,28 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
8490
return fmt.Sprintf(ls.UserDN, username), true
8591
}
8692

93+
func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
94+
// See http://tools.ietf.org/search/rfc4515
95+
badCharacters := "\x00*\\"
96+
if strings.ContainsAny(group, badCharacters) {
97+
log.Trace("Group filter invalid query characters: %s", group)
98+
return "", false
99+
}
100+
101+
return group, true
102+
}
103+
104+
func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
105+
// See http://tools.ietf.org/search/rfc4514: "special characters"
106+
badCharacters := "\x00()*\\'\"#+;<>"
107+
if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
108+
log.Trace("Group DN contains invalid query characters: %s", groupDn)
109+
return "", false
110+
}
111+
112+
return groupDn, true
113+
}
114+
87115
func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
88116
log.Trace("Search for LDAP user: %s", name)
89117

@@ -279,11 +307,14 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
279307
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
280308

281309
attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
310+
if len(strings.TrimSpace(ls.UserUID)) > 0 {
311+
attribs = append(attribs, ls.UserUID)
312+
}
282313
if isAttributeSSHPublicKeySet {
283314
attribs = append(attribs, ls.AttributeSSHPublicKey)
284315
}
285316

286-
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, userFilter, userDN)
317+
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.UserUID, userFilter, userDN)
287318
search := ldap.NewSearchRequest(
288319
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
289320
attribs, nil)
@@ -308,6 +339,51 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
308339
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
309340
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
310341
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
342+
uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
343+
344+
// Check group membership
345+
if ls.GroupsEnabled {
346+
groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
347+
if !ok {
348+
return nil
349+
}
350+
groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
351+
if !ok {
352+
return nil
353+
}
354+
355+
log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
356+
groupSearch := ldap.NewSearchRequest(
357+
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
358+
[]string{ls.GroupMemberUID},
359+
nil)
360+
361+
srg, err := l.Search(groupSearch)
362+
if err != nil {
363+
log.Error("LDAP group search failed: %v", err)
364+
return nil
365+
} else if len(srg.Entries) < 1 {
366+
log.Error("LDAP group search failed: 0 entries")
367+
return nil
368+
}
369+
370+
isMember := false
371+
Entries:
372+
for _, group := range srg.Entries {
373+
for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
374+
if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
375+
isMember = true
376+
break Entries
377+
}
378+
}
379+
}
380+
381+
if !isMember {
382+
log.Error("LDAP group membership test failed")
383+
return nil
384+
}
385+
}
386+
311387
if isAttributeSSHPublicKeySet {
312388
sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
313389
}

options/locale/locale_en-US.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,6 +2098,11 @@ auths.filter = User Filter
20982098
auths.admin_filter = Admin Filter
20992099
auths.restricted_filter = Restricted Filter
21002100
auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted.
2101+
auths.verify_group_membership = Verify group membership in LDAP
2102+
auths.group_search_base = Group Search Base DN
2103+
auths.valid_groups_filter = Valid Groups Filter
2104+
auths.group_attribute_list_users = Group Attribute Containing List Of Users
2105+
auths.user_attribute_in_group = User Attribute Listed In Group
21012106
auths.ms_ad_sa = MS AD Search Attributes
21022107
auths.smtp_auth = SMTP Authentication Type
21032108
auths.smtphost = SMTP Host

routers/admin/auths.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
129129
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
130130
SearchPageSize: pageSize,
131131
Filter: form.Filter,
132+
GroupsEnabled: form.GroupsEnabled,
133+
GroupDN: form.GroupDN,
134+
GroupFilter: form.GroupFilter,
135+
GroupMemberUID: form.GroupMemberUID,
136+
UserUID: form.UserUID,
132137
AdminFilter: form.AdminFilter,
133138
RestrictedFilter: form.RestrictedFilter,
134139
AllowDeactivateAll: form.AllowDeactivateAll,

templates/admin/auth/edit.tmpl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,31 @@
9999
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
100100
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="e.g. SshPublicKey">
101101
</div>
102+
<div class="inline field">
103+
<div class="ui checkbox">
104+
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
105+
<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}>
106+
</div>
107+
</div>
108+
<div id="groups_enabled_change">
109+
<div class="field">
110+
<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
111+
<input id="group_dn" name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
112+
</div>
113+
<div class="field">
114+
<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
115+
<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
116+
</div>
117+
<div class="field">
118+
<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
119+
<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid">
120+
</div>
121+
<div class="field">
122+
<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
123+
<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid">
124+
</div>
125+
<br/>
126+
</div>
102127
{{if .Source.IsLDAP}}
103128
<div class="inline field">
104129
<div class="ui checkbox">

templates/admin/auth/source/ldap.tmpl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@
7171
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
7272
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="e.g. SshPublicKey">
7373
</div>
74+
<div class="inline field">
75+
<div class="ui checkbox">
76+
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
77+
<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}>
78+
</div>
79+
</div>
80+
<div id="groups_enabled_change">
81+
<div class="field">
82+
<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
83+
<input id="group_dn" name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
84+
</div>
85+
<div class="field">
86+
<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
87+
<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
88+
</div>
89+
<div class="field">
90+
<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
91+
<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid">
92+
</div>
93+
<div class="field">
94+
<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
95+
<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid">
96+
</div>
97+
<br/>
98+
</div>
7499
<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
75100
<div class="ui checkbox">
76101
<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>

web_src/js/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,14 @@ function initAdmin() {
17951795
}
17961796
}
17971797

1798+
function onVerifyGroupMembershipChange() {
1799+
if ($('#groups_enabled').is(':checked')) {
1800+
$('#groups_enabled_change').show();
1801+
} else {
1802+
$('#groups_enabled_change').hide();
1803+
}
1804+
}
1805+
17981806
// New authentication
17991807
if ($('.admin.new.authentication').length > 0) {
18001808
$('#auth_type').on('change', function () {
@@ -1835,6 +1843,7 @@ function initAdmin() {
18351843
}
18361844
if (authType === '2' || authType === '5') {
18371845
onSecurityProtocolChange();
1846+
onVerifyGroupMembershipChange();
18381847
}
18391848
if (authType === '2') {
18401849
onUsePagedSearchChange();
@@ -1845,12 +1854,15 @@ function initAdmin() {
18451854
$('#use_paged_search').on('change', onUsePagedSearchChange);
18461855
$('#oauth2_provider').on('change', onOAuth2Change);
18471856
$('#oauth2_use_custom_url').on('change', onOAuth2UseCustomURLChange);
1857+
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
18481858
}
18491859
// Edit authentication
18501860
if ($('.admin.edit.authentication').length > 0) {
18511861
const authType = $('#auth_type').val();
18521862
if (authType === '2' || authType === '5') {
18531863
$('#security_protocol').on('change', onSecurityProtocolChange);
1864+
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
1865+
onVerifyGroupMembershipChange();
18541866
if (authType === '2') {
18551867
$('#use_paged_search').on('change', onUsePagedSearchChange);
18561868
}

0 commit comments

Comments
 (0)