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>release/v1.15
parent
4c42fce401
commit
c3e8c9441a
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue