Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for LDAP group membership #10869

Merged
merged 11 commits into from
Sep 10, 2020
5 changes: 5 additions & 0 deletions modules/auth/auth_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ type AuthenticationForm struct {
SearchPageSize int
Filter string
AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string
AllowDeactivateAll bool
IsActive bool
Expand Down
18 changes: 18 additions & 0 deletions modules/auth/ldap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,21 @@ share the following fields:
matching parameter will be substituted with the user's username.
* Example: (&(objectClass=posixAccount)(cn=%s))
* Example: (&(objectClass=posixAccount)(uid=%s))

**Verify group membership in LDAP** uses the following fields:

* Group Search Base (optional)
* The LDAP DN used for groups.
* Example: ou=group,dc=mydomain,dc=com

* Group Name Filter (optional)
* An LDAP filter declaring how to find valid groups in the above DN.
* Example: (|(cn=gitea_users)(cn=admins))

* User Attribute in Group (optional)
* Which user LDAP attribute is listed in the group.
* Example: uid

* Group Attribute for User (optional)
* Which group LDAP attribute contains an array above user attribute names.
* Example: memberUid
80 changes: 78 additions & 2 deletions modules/auth/ldap/ldap.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

Expand All @@ -13,7 +14,7 @@ import (

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

ldap "gopkg.in/ldap.v3"
"gopkg.in/ldap.v3"
)

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

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

func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00*\\"
if strings.ContainsAny(group, badCharacters) {
log.Trace("Group filter invalid query characters: %s", group)
return "", false
}

return group, true
}

func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\'\"#+;<>"
if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
log.Trace("Group DN contains invalid query characters: %s", groupDn)
return "", false
}

return groupDn, true
}

func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
log.Trace("Search for LDAP user: %s", name)

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

attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if len(strings.TrimSpace(ls.UserUID)) > 0 {
attribs = append(attribs, ls.UserUID)
}
if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey)
}

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)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID, userFilter, userDN)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
Expand All @@ -308,6 +339,51 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
uid := sr.Entries[0].GetAttributeValue(ls.UserUID)

// Check group membership
if ls.GroupsEnabled {
groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
if !ok {
return nil
}
groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
if !ok {
return nil
}

log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
groupSearch := ldap.NewSearchRequest(
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
[]string{ls.GroupMemberUID},
nil)

srg, err := l.Search(groupSearch)
if err != nil {
log.Error("LDAP group search failed: %v", err)
return nil
} else if len(srg.Entries) < 1 {
log.Error("LDAP group search failed: 0 entries")
return nil
}

isMember := false
Entries:
for _, group := range srg.Entries {
for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
isMember = true
break Entries
}
}
}
jolheiser marked this conversation as resolved.
Show resolved Hide resolved

if !isMember {
log.Error("LDAP group membership test failed")
return nil
}
}

if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
}
Expand Down
5 changes: 5 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,11 @@ auths.filter = User Filter
auths.admin_filter = Admin Filter
auths.restricted_filter = Restricted Filter
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.
auths.verify_group_membership = Verify group membership in LDAP
auths.group_search_base = Group Search Base DN
auths.valid_groups_filter = Valid Groups Filter
auths.group_attribute_list_users = Group Attribute Containing List Of Users
auths.user_attribute_in_group = User Attribute Listed In Group
auths.ms_ad_sa = MS AD Search Attributes
auths.smtp_auth = SMTP Authentication Type
auths.smtphost = SMTP Host
Expand Down
5 changes: 5 additions & 0 deletions routers/admin/auths.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
SearchPageSize: pageSize,
Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
GroupDN: form.GroupDN,
GroupFilter: form.GroupFilter,
GroupMemberUID: form.GroupMemberUID,
UserUID: form.UserUID,
AdminFilter: form.AdminFilter,
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Expand Down
25 changes: 25 additions & 0 deletions templates/admin/auth/edit.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,31 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="e.g. SshPublicKey">
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}>
</div>
</div>
<div id="groups_enabled_change">
<div class="field">
<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
<input id="group_dn" name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
</div>
<div class="field">
<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
</div>
<div class="field">
<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid">
</div>
<div class="field">
<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid">
</div>
<br/>
</div>
{{if .Source.IsLDAP}}
<div class="inline field">
<div class="ui checkbox">
Expand Down
25 changes: 25 additions & 0 deletions templates/admin/auth/source/ldap.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label>
<input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="e.g. SshPublicKey">
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}>
</div>
</div>
<div id="groups_enabled_change">
<div class="field">
<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
<input id="group_dn" name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
</div>
<div class="field">
<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
</div>
<div class="field">
<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid">
</div>
<div class="field">
<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid">
</div>
<br/>
</div>
<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
<div class="ui checkbox">
<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>
Expand Down
12 changes: 12 additions & 0 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,14 @@ function initAdmin() {
}
}

function onVerifyGroupMembershipChange() {
if ($('#groups_enabled').is(':checked')) {
$('#groups_enabled_change').show();
} else {
$('#groups_enabled_change').hide();
}
}

// New authentication
if ($('.admin.new.authentication').length > 0) {
$('#auth_type').on('change', function () {
Expand Down Expand Up @@ -1838,6 +1846,7 @@ function initAdmin() {
}
if (authType === '2' || authType === '5') {
onSecurityProtocolChange();
onVerifyGroupMembershipChange();
}
if (authType === '2') {
onUsePagedSearchChange();
Expand All @@ -1848,12 +1857,15 @@ function initAdmin() {
$('#use_paged_search').on('change', onUsePagedSearchChange);
$('#oauth2_provider').on('change', onOAuth2Change);
$('#oauth2_use_custom_url').on('change', onOAuth2UseCustomURLChange);
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
}
// Edit authentication
if ($('.admin.edit.authentication').length > 0) {
const authType = $('#auth_type').val();
if (authType === '2' || authType === '5') {
$('#security_protocol').on('change', onSecurityProtocolChange);
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
onVerifyGroupMembershipChange();
if (authType === '2') {
$('#use_paged_search').on('change', onUsePagedSearchChange);
}
Expand Down