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 <john.olheiser@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
John Olheiser 2020-09-10 10:30:07 -05:00 committed by GitHub
parent 4c42fce401
commit c3e8c9441a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 173 additions and 2 deletions

View file

@ -30,6 +30,11 @@ type AuthenticationForm struct {
SearchPageSize int SearchPageSize int
Filter string Filter string
AdminFilter string AdminFilter string
GroupsEnabled bool
GroupDN string
GroupFilter string
GroupMemberUID string
UserUID string
RestrictedFilter string RestrictedFilter string
AllowDeactivateAll bool AllowDeactivateAll bool
IsActive bool IsActive bool

View file

@ -103,3 +103,21 @@ share the following fields:
matching parameter will be substituted with the user's username. matching parameter will be substituted with the user's username.
* Example: (&(objectClass=posixAccount)(cn=%s)) * Example: (&(objectClass=posixAccount)(cn=%s))
* Example: (&(objectClass=posixAccount)(uid=%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

View file

@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved. // 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 // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -13,7 +14,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
ldap "gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
) )
// SecurityProtocol protocol type // SecurityProtocol protocol type
@ -49,6 +50,11 @@ type Source struct {
RestrictedFilter string // Query filter to check if user is restricted RestrictedFilter string // Query filter to check if user is restricted
Enabled bool // if this source is disabled Enabled bool // if this source is disabled
AllowDeactivateAll bool // Allow an empty search response to deactivate all users from this source 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 // SearchResult : user data
@ -84,6 +90,28 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
return fmt.Sprintf(ls.UserDN, username), true 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) { func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
log.Trace("Search for LDAP user: %s", name) log.Trace("Search for LDAP user: %s", name)
@ -279,11 +307,14 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0 var isAttributeSSHPublicKeySet = len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail} attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
if len(strings.TrimSpace(ls.UserUID)) > 0 {
attribs = append(attribs, ls.UserUID)
}
if isAttributeSSHPublicKeySet { if isAttributeSSHPublicKeySet {
attribs = append(attribs, ls.AttributeSSHPublicKey) 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) 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)
search := ldap.NewSearchRequest( search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil) attribs, nil)
@ -308,6 +339,51 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName) firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname) surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail) 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
}
}
}
if !isMember {
log.Error("LDAP group membership test failed")
return nil
}
}
if isAttributeSSHPublicKeySet { if isAttributeSSHPublicKeySet {
sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey) sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
} }

View file

@ -2098,6 +2098,11 @@ auths.filter = User Filter
auths.admin_filter = Admin Filter auths.admin_filter = Admin Filter
auths.restricted_filter = Restricted 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.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.ms_ad_sa = MS AD Search Attributes
auths.smtp_auth = SMTP Authentication Type auths.smtp_auth = SMTP Authentication Type
auths.smtphost = SMTP Host auths.smtphost = SMTP Host

View file

@ -129,6 +129,11 @@ func parseLDAPConfig(form auth.AuthenticationForm) *models.LDAPConfig {
AttributeSSHPublicKey: form.AttributeSSHPublicKey, AttributeSSHPublicKey: form.AttributeSSHPublicKey,
SearchPageSize: pageSize, SearchPageSize: pageSize,
Filter: form.Filter, Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
GroupDN: form.GroupDN,
GroupFilter: form.GroupFilter,
GroupMemberUID: form.GroupMemberUID,
UserUID: form.UserUID,
AdminFilter: form.AdminFilter, AdminFilter: form.AdminFilter,
RestrictedFilter: form.RestrictedFilter, RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll, AllowDeactivateAll: form.AllowDeactivateAll,

View file

@ -99,6 +99,31 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label> <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"> <input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="e.g. SshPublicKey">
</div> </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}} {{if .Source.IsLDAP}}
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">

View file

@ -71,6 +71,31 @@
<label for="attribute_ssh_public_key">{{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}</label> <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"> <input id="attribute_ssh_public_key" name="attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="e.g. SshPublicKey">
</div> </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="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
<div class="ui checkbox"> <div class="ui checkbox">
<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label> <label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>

View file

@ -1795,6 +1795,14 @@ function initAdmin() {
} }
} }
function onVerifyGroupMembershipChange() {
if ($('#groups_enabled').is(':checked')) {
$('#groups_enabled_change').show();
} else {
$('#groups_enabled_change').hide();
}
}
// New authentication // New authentication
if ($('.admin.new.authentication').length > 0) { if ($('.admin.new.authentication').length > 0) {
$('#auth_type').on('change', function () { $('#auth_type').on('change', function () {
@ -1835,6 +1843,7 @@ function initAdmin() {
} }
if (authType === '2' || authType === '5') { if (authType === '2' || authType === '5') {
onSecurityProtocolChange(); onSecurityProtocolChange();
onVerifyGroupMembershipChange();
} }
if (authType === '2') { if (authType === '2') {
onUsePagedSearchChange(); onUsePagedSearchChange();
@ -1845,12 +1854,15 @@ function initAdmin() {
$('#use_paged_search').on('change', onUsePagedSearchChange); $('#use_paged_search').on('change', onUsePagedSearchChange);
$('#oauth2_provider').on('change', onOAuth2Change); $('#oauth2_provider').on('change', onOAuth2Change);
$('#oauth2_use_custom_url').on('change', onOAuth2UseCustomURLChange); $('#oauth2_use_custom_url').on('change', onOAuth2UseCustomURLChange);
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
} }
// Edit authentication // Edit authentication
if ($('.admin.edit.authentication').length > 0) { if ($('.admin.edit.authentication').length > 0) {
const authType = $('#auth_type').val(); const authType = $('#auth_type').val();
if (authType === '2' || authType === '5') { if (authType === '2' || authType === '5') {
$('#security_protocol').on('change', onSecurityProtocolChange); $('#security_protocol').on('change', onSecurityProtocolChange);
$('#groups_enabled').on('change', onVerifyGroupMembershipChange);
onVerifyGroupMembershipChange();
if (authType === '2') { if (authType === '2') {
$('#use_paged_search').on('change', onUsePagedSearchChange); $('#use_paged_search').on('change', onUsePagedSearchChange);
} }