GUACAMOLE-220: Retrieve user groups from LDAP. Take immediate group membership into account.

This commit is contained in:
Michael Jumper
2018-11-03 12:34:04 -07:00
parent bdc792603d
commit aa0c654231
6 changed files with 300 additions and 44 deletions

View File

@@ -23,9 +23,11 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPConnection;
import java.util.List; import java.util.List;
import java.util.Set;
import org.apache.guacamole.auth.ldap.user.AuthenticatedUser; import org.apache.guacamole.auth.ldap.user.AuthenticatedUser;
import org.apache.guacamole.auth.ldap.user.UserContext; import org.apache.guacamole.auth.ldap.user.UserContext;
import org.apache.guacamole.GuacamoleException; 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.auth.ldap.user.UserService;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
@@ -62,6 +64,12 @@ public class AuthenticationProviderService {
@Inject @Inject
private UserService userService; private UserService userService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/** /**
* Provider for AuthenticatedUser objects. * Provider for AuthenticatedUser objects.
*/ */
@@ -222,9 +230,14 @@ public class AuthenticationProviderService {
try { try {
// Retrieve group membership of the user that just authenticated
Set<String> effectiveGroups =
userGroupService.getParentUserGroupIdentifiers(ldapConnection,
ldapConnection.getAuthenticationDN());
// Return AuthenticatedUser if bind succeeds // Return AuthenticatedUser if bind succeeds
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials); authenticatedUser.init(credentials, effectiveGroups);
return authenticatedUser; return authenticatedUser;
} }

View File

@@ -23,6 +23,7 @@ import com.google.inject.AbstractModule;
import org.apache.guacamole.auth.ldap.connection.ConnectionService; import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.auth.ldap.user.UserService; import org.apache.guacamole.auth.ldap.user.UserService;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -78,6 +79,7 @@ public class LDAPAuthenticationProviderModule extends AbstractModule {
bind(EscapingService.class); bind(EscapingService.class);
bind(LDAPConnectionService.class); bind(LDAPConnectionService.class);
bind(ObjectQueryService.class); bind(ObjectQueryService.class);
bind(UserGroupService.class);
bind(UserService.class); bind(UserService.class);
} }

View File

@@ -24,8 +24,6 @@ import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPReferralException;
import com.novell.ldap.LDAPSearchResults;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
@@ -36,6 +34,7 @@ import org.apache.guacamole.auth.ldap.EscapingService;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.ObjectQueryService; 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.AuthenticatedUser;
import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.simple.SimpleConnection; import org.apache.guacamole.net.auth.simple.SimpleConnection;
@@ -74,6 +73,12 @@ public class ConnectionService {
@Inject @Inject
private ObjectQueryService queryService; private ObjectQueryService queryService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/** /**
* Returns all Guacamole connections accessible to the user currently bound * Returns all Guacamole connections accessible to the user currently bound
* under the given LDAP connection. * under the given LDAP connection.
@@ -226,43 +231,12 @@ public class ConnectionService {
connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN)); connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN));
connectionSearchFilter.append(")"); connectionSearchFilter.append(")");
// If group base DN is specified search for user groups // Additionally filter by group membership if the current user is a
String groupBaseDN = confService.getGroupBaseDN(); // member of any user groups
if (groupBaseDN != null) { List<LDAPEntry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
if (!userGroups.isEmpty()) {
// Get all groups the user is a member of starting at the groupBaseDN, excluding guacConfigGroups for (LDAPEntry entry : userGroups)
LDAPSearchResults userRoleGroupResults = ldapConnection.search( connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
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);
}
}
}
} }
// Complete the search filter. // Complete the search filter.

View File

@@ -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<String, UserGroup> 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.<String, UserGroup>emptyMap();
// Retrieve all visible user groups which are not guacConfigGroups
Collection<String> attributes = confService.getGroupNameAttributes();
List<LDAPEntry> 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<LDAPEntry> 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.<LDAPEntry>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<String> getParentUserGroupIdentifiers(LDAPConnection ldapConnection,
String userDN) throws GuacamoleException {
Collection<String> attributes = confService.getGroupNameAttributes();
List<LDAPEntry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
Set<String> 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;
}
}

View File

@@ -20,6 +20,7 @@
package org.apache.guacamole.auth.ldap.user; package org.apache.guacamole.auth.ldap.user;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Set;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser; import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
@@ -43,13 +44,25 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser {
private Credentials credentials; 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<String> effectiveGroups;
/**
* Initializes this AuthenticatedUser with the given credentials and set of
* effective user groups.
* *
* @param credentials * @param credentials
* The credentials provided when this user was authenticated. * 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<String> effectiveGroups) {
this.credentials = credentials; this.credentials = credentials;
this.effectiveGroups = effectiveGroups;
setIdentifier(credentials.getUsername()); setIdentifier(credentials.getUsername());
} }
@@ -63,4 +76,9 @@ public class AuthenticatedUser extends AbstractAuthenticatedUser {
return credentials; return credentials;
} }
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
} }

View File

@@ -25,6 +25,7 @@ import java.util.Collections;
import org.apache.guacamole.auth.ldap.connection.ConnectionService; import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider; 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.AbstractUserContext;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider; 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.ConnectionGroup;
import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User; 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.SimpleConnectionGroup;
import org.apache.guacamole.net.auth.simple.SimpleDirectory; import org.apache.guacamole.net.auth.simple.SimpleDirectory;
import org.apache.guacamole.net.auth.simple.SimpleUser; import org.apache.guacamole.net.auth.simple.SimpleUser;
@@ -61,6 +63,12 @@ public class UserContext extends AbstractUserContext {
@Inject @Inject
private UserService userService; private UserService userService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/** /**
* Reference to the AuthenticationProvider associated with this * Reference to the AuthenticationProvider associated with this
* UserContext. * UserContext.
@@ -80,6 +88,12 @@ public class UserContext extends AbstractUserContext {
*/ */
private Directory<User> userDirectory; private Directory<User> userDirectory;
/**
* Directory containing all UserGroup objects accessible to the user
* associated with this UserContext.
*/
private Directory<UserGroup> userGroupDirectory;
/** /**
* Directory containing all Connection objects accessible to the user * Directory containing all Connection objects accessible to the user
* associated with this UserContext. * associated with this UserContext.
@@ -112,12 +126,17 @@ public class UserContext extends AbstractUserContext {
throws GuacamoleException { throws GuacamoleException {
// Query all accessible users // Query all accessible users
userDirectory = new SimpleDirectory<User>( userDirectory = new SimpleDirectory<>(
userService.getUsers(ldapConnection) userService.getUsers(ldapConnection)
); );
// Query all accessible user groups
userGroupDirectory = new SimpleDirectory<>(
userGroupService.getUserGroups(ldapConnection)
);
// Query all accessible connections // Query all accessible connections
connectionDirectory = new SimpleDirectory<Connection>( connectionDirectory = new SimpleDirectory<>(
connectionService.getConnections(user, ldapConnection) connectionService.getConnections(user, ldapConnection)
); );
@@ -133,6 +152,7 @@ public class UserContext extends AbstractUserContext {
self = new SimpleUser( self = new SimpleUser(
user.getIdentifier(), user.getIdentifier(),
userDirectory.getIdentifiers(), userDirectory.getIdentifiers(),
userGroupDirectory.getIdentifiers(),
connectionDirectory.getIdentifiers(), connectionDirectory.getIdentifiers(),
Collections.singleton(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP) Collections.singleton(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP)
); );
@@ -154,6 +174,11 @@ public class UserContext extends AbstractUserContext {
return userDirectory; return userDirectory;
} }
@Override
public Directory<UserGroup> getUserGroupDirectory() throws GuacamoleException {
return userGroupDirectory;
}
@Override @Override
public Directory<Connection> getConnectionDirectory() public Directory<Connection> getConnectionDirectory()
throws GuacamoleException { throws GuacamoleException {