From aa0c65423146929a46ceeb1beb7573815c0e4513 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 3 Nov 2018 12:34:04 -0700 Subject: [PATCH] GUACAMOLE-220: Retrieve user groups from LDAP. Take immediate group membership into account. --- .../ldap/AuthenticationProviderService.java | 15 +- .../LDAPAuthenticationProviderModule.java | 2 + .../ldap/connection/ConnectionService.java | 52 +--- .../auth/ldap/group/UserGroupService.java | 224 ++++++++++++++++++ .../auth/ldap/user/AuthenticatedUser.java | 22 +- .../guacamole/auth/ldap/user/UserContext.java | 29 ++- 6 files changed, 300 insertions(+), 44 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java index a25c697e6..4a746f11a 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java @@ -23,9 +23,11 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.novell.ldap.LDAPConnection; import java.util.List; +import java.util.Set; import org.apache.guacamole.auth.ldap.user.AuthenticatedUser; import org.apache.guacamole.auth.ldap.user.UserContext; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.auth.ldap.user.UserService; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; @@ -62,6 +64,12 @@ public class AuthenticationProviderService { @Inject private UserService userService; + /** + * Service for retrieving user groups. + */ + @Inject + private UserGroupService userGroupService; + /** * Provider for AuthenticatedUser objects. */ @@ -222,9 +230,14 @@ public class AuthenticationProviderService { try { + // Retrieve group membership of the user that just authenticated + Set effectiveGroups = + userGroupService.getParentUserGroupIdentifiers(ldapConnection, + ldapConnection.getAuthenticationDN()); + // Return AuthenticatedUser if bind succeeds AuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(credentials); + authenticatedUser.init(credentials, effectiveGroups); return authenticatedUser; } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java index 547808060..23decec6d 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java @@ -23,6 +23,7 @@ import com.google.inject.AbstractModule; import org.apache.guacamole.auth.ldap.connection.ConnectionService; import org.apache.guacamole.auth.ldap.user.UserService; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.net.auth.AuthenticationProvider; @@ -78,6 +79,7 @@ public class LDAPAuthenticationProviderModule extends AbstractModule { bind(EscapingService.class); bind(LDAPConnectionService.class); bind(ObjectQueryService.class); + bind(UserGroupService.class); bind(UserService.class); } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java index 78100a0fe..bae1da813 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java @@ -24,8 +24,6 @@ import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; -import com.novell.ldap.LDAPReferralException; -import com.novell.ldap.LDAPSearchResults; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -36,6 +34,7 @@ import org.apache.guacamole.auth.ldap.EscapingService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ldap.ObjectQueryService; +import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.simple.SimpleConnection; @@ -74,6 +73,12 @@ public class ConnectionService { @Inject private ObjectQueryService queryService; + /** + * Service for retrieving user groups. + */ + @Inject + private UserGroupService userGroupService; + /** * Returns all Guacamole connections accessible to the user currently bound * under the given LDAP connection. @@ -226,43 +231,12 @@ public class ConnectionService { connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN)); connectionSearchFilter.append(")"); - // If group base DN is specified search for user groups - String groupBaseDN = confService.getGroupBaseDN(); - if (groupBaseDN != null) { - - // Get all groups the user is a member of starting at the groupBaseDN, excluding guacConfigGroups - LDAPSearchResults userRoleGroupResults = ldapConnection.search( - groupBaseDN, - LDAPConnection.SCOPE_SUB, - "(&(!(objectClass=guacConfigGroup))(member=" + escapingService.escapeLDAPSearchFilter(userDN) + "))", - null, - false, - confService.getLDAPSearchConstraints() - ); - - // Append the additional user groups to the LDAP filter - // Now the filter will also look for guacConfigGroups that refer - // to groups the user is a member of - // The guacConfig group uses the seeAlso attribute to refer - // to these other groups - while (userRoleGroupResults.hasMore()) { - try { - LDAPEntry entry = userRoleGroupResults.next(); - connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")"); - } - - catch (LDAPReferralException e) { - if (confService.getFollowReferrals()) { - logger.error("Could not follow referral: {}", e.getFailedReferral()); - logger.debug("Error encountered trying to follow referral.", e); - throw new GuacamoleServerException("Could not follow LDAP referral.", e); - } - else { - logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage()); - logger.debug("Got a referral, but configured to not follow them.", e); - } - } - } + // Additionally filter by group membership if the current user is a + // member of any user groups + List userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN); + if (!userGroups.isEmpty()) { + for (LDAPEntry entry : userGroups) + connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")"); } // Complete the search filter. diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java new file mode 100644 index 000000000..dfdd9fd92 --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ldap.group; + +import com.google.inject.Inject; +import com.novell.ldap.LDAPConnection; +import com.novell.ldap.LDAPEntry; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.guacamole.auth.ldap.ConfigurationService; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ldap.ObjectQueryService; +import org.apache.guacamole.net.auth.UserGroup; +import org.apache.guacamole.net.auth.simple.SimpleUserGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service for querying user group membership and retrieving user groups + * visible to a particular Guacamole user. + */ +public class UserGroupService { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(UserGroupService.class); + + /** + * Service for retrieving LDAP server configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Service for executing LDAP queries. + */ + @Inject + private ObjectQueryService queryService; + + /** + * Returns the base search filter which should be used to retrieve user + * groups which do not represent Guacamole connections. As excluding the + * guacConfigGroup object class may not work as expected (may always return + * zero results) if guacConfigGroup object class is not defined, it should + * only be explicitly excluded if it is expected to have been defined. + * + * @return + * The base search filter which should be used to retrieve user groups. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + private String getGroupSearchFilter() throws GuacamoleException { + + // Explicitly exclude guacConfigGroup object class only if it should + // be assumed to be defined (query may fail due to no such object + // class existing otherwise) + if (confService.getConfigurationBaseDN() != null) + return "(!(objectClass=guacConfigGroup))"; + + // Read any object as a group if LDAP is not being used for connection + // storage (guacConfigGroup) + return "(objectClass=*)"; + + } + + /** + * Returns all Guacamole user groups accessible to the user currently bound + * under the given LDAP connection. + * + * @param ldapConnection + * The current connection to the LDAP server, associated with the + * current user. + * + * @return + * All user groups accessible to the user currently bound under the + * given LDAP connection, as a map of user group identifier to + * corresponding UserGroup object. + * + * @throws GuacamoleException + * If an error occurs preventing retrieval of user groups. + */ + public Map getUserGroups(LDAPConnection ldapConnection) + throws GuacamoleException { + + // Do not return any user groups if base DN is not specified + String groupBaseDN = confService.getGroupBaseDN(); + if (groupBaseDN == null) + return Collections.emptyMap(); + + // Retrieve all visible user groups which are not guacConfigGroups + Collection attributes = confService.getGroupNameAttributes(); + List results = queryService.search( + ldapConnection, + groupBaseDN, + getGroupSearchFilter(), + attributes, + null + ); + + // Convert retrieved user groups to map of identifier to Guacamole + // user group object + return queryService.asMap(results, entry -> { + + // Translate entry into UserGroup object having proper identifier + String name = queryService.getIdentifier(entry, attributes); + if (name != null) + return new SimpleUserGroup(name); + + // Ignore user groups which lack a name attribute + logger.debug("User group \"{}\" is missing a name attribute " + + "and will be ignored.", entry.getDN()); + return null; + + }); + + } + + /** + * Returns the LDAP entries representing all user groups that the given + * user is a member of. Only user groups which are readable by the current + * user will be retrieved. + * + * @param ldapConnection + * The current connection to the LDAP server, associated with the + * current user. + * + * @param userDN + * The DN of the user whose group membership should be retrieved. + * + * @return + * The LDAP entries representing all readable parent user groups of the + * user having the given DN. + * + * @throws GuacamoleException + * If an error occurs preventing retrieval of user groups. + */ + public List getParentUserGroupEntries(LDAPConnection ldapConnection, + String userDN) throws GuacamoleException { + + // Do not return any user groups if base DN is not specified + String groupBaseDN = confService.getGroupBaseDN(); + if (groupBaseDN == null) + return Collections.emptyList(); + + // Get all groups the user is a member of starting at the groupBaseDN, + // excluding guacConfigGroups + return queryService.search( + ldapConnection, + groupBaseDN, + getGroupSearchFilter(), + Collections.singleton("member"), + userDN + ); + + } + + /** + * Returns the identifiers of all user groups that the given user is a + * member of. Only identifiers of user groups which are readable by the + * current user will be retrieved. + * + * @param ldapConnection + * The current connection to the LDAP server, associated with the + * current user. + * + * @param userDN + * The DN of the user whose group membership should be retrieved. + * + * @return + * The identifiers of all readable parent user groups of the user + * having the given DN. + * + * @throws GuacamoleException + * If an error occurs preventing retrieval of user groups. + */ + public Set getParentUserGroupIdentifiers(LDAPConnection ldapConnection, + String userDN) throws GuacamoleException { + + Collection attributes = confService.getGroupNameAttributes(); + List userGroups = getParentUserGroupEntries(ldapConnection, userDN); + + Set identifiers = new HashSet<>(userGroups.size()); + userGroups.forEach(entry -> { + + // Determine unique identifier for user group + String name = queryService.getIdentifier(entry, attributes); + if (name != null) + identifiers.add(name); + + // Ignore user groups which lack a name attribute + else + logger.debug("User group \"{}\" is missing a name attribute " + + "and will be ignored.", entry.getDN()); + + }); + + return identifiers; + + } + +} diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java index 669efcd54..85f004b09 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.ldap.user; import com.google.inject.Inject; +import java.util.Set; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -43,13 +44,25 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser { private Credentials credentials; /** - * Initializes this AuthenticatedUser using the given credentials. + * The unique identifiers of all user groups which affect the permissions + * available to this user. + */ + private Set effectiveGroups; + + /** + * Initializes this AuthenticatedUser with the given credentials and set of + * effective user groups. * * @param credentials * The credentials provided when this user was authenticated. + * + * @param effectiveGroups + * The unique identifiers of all user groups which affect the + * permissions available to this user. */ - public void init(Credentials credentials) { + public void init(Credentials credentials, Set effectiveGroups) { this.credentials = credentials; + this.effectiveGroups = effectiveGroups; setIdentifier(credentials.getUsername()); } @@ -63,4 +76,9 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser { return credentials; } + @Override + public Set getEffectiveUserGroups() { + return effectiveGroups; + } + } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.java index 26ea6b319..7c520d314 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.java @@ -25,6 +25,7 @@ import java.util.Collections; import org.apache.guacamole.auth.ldap.connection.ConnectionService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; +import org.apache.guacamole.auth.ldap.group.UserGroupService; import org.apache.guacamole.net.auth.AbstractUserContext; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; @@ -32,6 +33,7 @@ import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.simple.SimpleConnectionGroup; import org.apache.guacamole.net.auth.simple.SimpleDirectory; import org.apache.guacamole.net.auth.simple.SimpleUser; @@ -61,6 +63,12 @@ public class UserContext extends AbstractUserContext { @Inject private UserService userService; + /** + * Service for retrieving user groups. + */ + @Inject + private UserGroupService userGroupService; + /** * Reference to the AuthenticationProvider associated with this * UserContext. @@ -80,6 +88,12 @@ public class UserContext extends AbstractUserContext { */ private Directory userDirectory; + /** + * Directory containing all UserGroup objects accessible to the user + * associated with this UserContext. + */ + private Directory userGroupDirectory; + /** * Directory containing all Connection objects accessible to the user * associated with this UserContext. @@ -112,12 +126,17 @@ public class UserContext extends AbstractUserContext { throws GuacamoleException { // Query all accessible users - userDirectory = new SimpleDirectory( + userDirectory = new SimpleDirectory<>( userService.getUsers(ldapConnection) ); + // Query all accessible user groups + userGroupDirectory = new SimpleDirectory<>( + userGroupService.getUserGroups(ldapConnection) + ); + // Query all accessible connections - connectionDirectory = new SimpleDirectory( + connectionDirectory = new SimpleDirectory<>( connectionService.getConnections(user, ldapConnection) ); @@ -133,6 +152,7 @@ public class UserContext extends AbstractUserContext { self = new SimpleUser( user.getIdentifier(), userDirectory.getIdentifiers(), + userGroupDirectory.getIdentifiers(), connectionDirectory.getIdentifiers(), Collections.singleton(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP) ); @@ -154,6 +174,11 @@ public class UserContext extends AbstractUserContext { return userDirectory; } + @Override + public Directory getUserGroupDirectory() throws GuacamoleException { + return userGroupDirectory; + } + @Override public Directory getConnectionDirectory() throws GuacamoleException {