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:
		
							parent
							
								
									4c42fce401
								
							
						
					
					
						commit
						c3e8c9441a
					
				
					 8 changed files with 173 additions and 2 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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.
 | ||||
| 
 | ||||
|  | @ -13,7 +14,7 @@ import ( | |||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 
 | ||||
| 	ldap "gopkg.in/ldap.v3" | ||||
| 	"gopkg.in/ldap.v3" | ||||
| ) | ||||
| 
 | ||||
| // SecurityProtocol protocol type
 | ||||
|  | @ -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
 | ||||
|  | @ -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) | ||||
| 
 | ||||
|  | @ -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) | ||||
| 	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( | ||||
| 		userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, | ||||
| 		attribs, nil) | ||||
|  | @ -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 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if !isMember { | ||||
| 			log.Error("LDAP group membership test failed") | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if isAttributeSSHPublicKeySet { | ||||
| 		sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey) | ||||
| 	} | ||||
|  |  | |||
|  | @ -2098,6 +2098,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 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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"> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -1795,6 +1795,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 () { | ||||
|  | @ -1835,6 +1843,7 @@ function initAdmin() { | |||
|       } | ||||
|       if (authType === '2' || authType === '5') { | ||||
|         onSecurityProtocolChange(); | ||||
|         onVerifyGroupMembershipChange(); | ||||
|       } | ||||
|       if (authType === '2') { | ||||
|         onUsePagedSearchChange(); | ||||
|  | @ -1845,12 +1854,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); | ||||
|       } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue