GUACAMOLE-793: validateTicket() returns the CASAuthenticatedUser instance rather than just a token so CAS Provider can return Group - like LDAP Provider

This commit is contained in:
Ron Record
2020-12-05 22:09:45 -08:00
committed by Michael Jumper
parent ecd385b0f8
commit 7b8dc36644
5 changed files with 130 additions and 23 deletions

View File

@@ -20,9 +20,9 @@
package org.apache.guacamole.auth.cas; package org.apache.guacamole.auth.cas;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.form.Field; import org.apache.guacamole.form.Field;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
@@ -34,6 +34,8 @@ import org.apache.guacamole.auth.cas.form.CASTicketField;
import org.apache.guacamole.auth.cas.ticket.TicketValidationService; import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser; import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.language.TranslatableMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Service providing convenience functions for the CAS AuthenticationProvider * Service providing convenience functions for the CAS AuthenticationProvider
@@ -41,6 +43,11 @@ import org.apache.guacamole.language.TranslatableMessage;
*/ */
public class AuthenticationProviderService { public class AuthenticationProviderService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
/** /**
* Service for retrieving CAS configuration information. * Service for retrieving CAS configuration information.
*/ */
@@ -53,12 +60,6 @@ public class AuthenticationProviderService {
@Inject @Inject
private TicketValidationService ticketService; private TicketValidationService ticketService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<CASAuthenticatedUser> authenticatedUserProvider;
/** /**
* Returns an AuthenticatedUser representing the user authenticated by the * Returns an AuthenticatedUser representing the user authenticated by the
* given credentials. * given credentials.
@@ -82,13 +83,7 @@ public class AuthenticationProviderService {
if (request != null) { if (request != null) {
String ticket = request.getParameter(CASTicketField.PARAMETER_NAME); String ticket = request.getParameter(CASTicketField.PARAMETER_NAME);
if (ticket != null) { if (ticket != null) {
Map<String, String> tokens = ticketService.validateTicket(ticket, credentials); return ticketService.validateTicket(ticket, credentials);
String username = credentials.getUsername();
if (username != null) {
CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials, tokens);
return authenticatedUser;
}
} }
} }

View File

@@ -20,6 +20,7 @@
package org.apache.guacamole.auth.cas.conf; package org.apache.guacamole.auth.cas.conf;
import org.apache.guacamole.properties.URIGuacamoleProperty; import org.apache.guacamole.properties.URIGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
/** /**
* Provides properties required for use of the CAS authentication provider. * Provides properties required for use of the CAS authentication provider.
@@ -69,4 +70,27 @@ public class CASGuacamoleProperties {
}; };
/**
* The attribute used for group membership
* example: memberOf (case sensitive)
*/
public static final StringGuacamoleProperty CAS_GROUP_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "cas-group-attribute"; }
};
/**
* The name of the attribute used for group membership, such as "memberOf".
* This attribute is case sensitive.
*/
public static final StringGuacamoleProperty CAS_GROUP_DN_FORMAT =
new StringGuacamoleProperty() {
@Override
public String getName() { return "cas-group-dn-format"; }
};
} }

View File

@@ -85,4 +85,40 @@ public class ConfigurationService {
return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY); return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY);
} }
/**
* Returns the attribute used to determine group memberships
* in CAS, or null if not defined.
*
* @return
* The attribute name used to determine group memberships in CAS,
* null if not defined.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupAttribute() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_ATTRIBUTE);
}
/**
* Returns the full DN specification used to format group DN's in CAS,
* or null if not defined.
* If CAS is backed by LDAP, it will return an LDAP DN, such as
* CN=foo,OU=bar,DC=example,DC=com. This DN specification may be set to
* CN=%s,OU=bar,DC=example,DC=com and given the example above, would result
* in a group called "foo". CAS backed by something other than LDAP would
* likely not need this.
*
* @return
* The DN format specification.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupDnFormat() throws GuacamoleException {
return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_DN_FORMAT);
}
} }

View File

@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.cas.ticket;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider;
import java.net.URI; import java.net.URI;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@@ -33,10 +34,15 @@ import java.nio.charset.Charset;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.cas.conf.ConfigurationService; import org.apache.guacamole.auth.cas.conf.ConfigurationService;
import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.token.TokenName; import org.apache.guacamole.token.TokenName;
import org.jasig.cas.client.authentication.AttributePrincipal; import org.jasig.cas.client.authentication.AttributePrincipal;
@@ -68,6 +74,12 @@ public class TicketValidationService {
@Inject @Inject
private ConfigurationService confService; private ConfigurationService confService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<CASAuthenticatedUser> authenticatedUserProvider;
/** /**
* Validates and parses the given ID ticket, returning a map of all * Validates and parses the given ID ticket, returning a map of all
* available tokens for the given user based on attributes provided by the * available tokens for the given user based on attributes provided by the
@@ -81,14 +93,13 @@ public class TicketValidationService {
* password values in. * password values in.
* *
* @return * @return
* A Map all of tokens for the user parsed from attributes returned * A CASAuthenticatedUser instance containing the ticket data returned by the CAS server.
* by the CAS server.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the ID ticket is not valid or guacamole.properties could * If the ID ticket is not valid or guacamole.properties could
* not be parsed. * not be parsed.
*/ */
public Map<String, String> validateTicket(String ticket, public CASAuthenticatedUser validateTicket(String ticket,
Credentials credentials) throws GuacamoleException { Credentials credentials) throws GuacamoleException {
// Retrieve the configured CAS URL, establish a ticket validator, // Retrieve the configured CAS URL, establish a ticket validator,
@@ -100,6 +111,7 @@ public class TicketValidationService {
validator.setEncoding("UTF-8"); validator.setEncoding("UTF-8");
try { try {
Map<String, String> tokens = new HashMap<>(); Map<String, String> tokens = new HashMap<>();
Set<String> effectiveGroups = new HashSet<>();
URI confRedirectURI = confService.getRedirectURI(); URI confRedirectURI = confService.getRedirectURI();
Assertion a = validator.validate(ticket, confRedirectURI.toString()); Assertion a = validator.validate(ticket, confRedirectURI.toString());
AttributePrincipal principal = a.getPrincipal(); AttributePrincipal principal = a.getPrincipal();
@@ -122,16 +134,44 @@ public class TicketValidationService {
} }
// Convert remaining attributes that have values to Strings // Convert remaining attributes that have values to Strings
String groupAttribute = confService.getGroupAttribute();
// Use cas-member-attribute to retrieve and set group memberships
String groupDnFormat = confService.getGroupDnFormat();
String groupTemplate = "";
if (groupDnFormat != null) {
// if CAS is backended to LDAP, groups come in as RFC4514 DN
// syntax. If cas-group-dn-format is set, this strips an
// entry such as "CN=Foo,OU=Bar,DC=example,DC=com" to "Foo"
groupTemplate = groupDnFormat.replace("%s","([A-Za-z0-9_\\(\\)\\-\\.\\s+]+)");
// the underlying parser aggregates all instances of the same
// attribute, so we need to be able to parse them out
groupTemplate=groupTemplate+",*\\s*";
}
else {
groupTemplate = "([A-Za-z0-9_\\(\\)\\-\\.\\s+]+,*\\s*)";
}
Pattern pattern = Pattern.compile(groupTemplate);
for (Entry <String, Object> attr : ticketAttrs.entrySet()) { for (Entry <String, Object> attr : ticketAttrs.entrySet()) {
String tokenName = TokenName.canonicalize(attr.getKey(), String tokenName = TokenName.canonicalize(attr.getKey(),
CAS_ATTRIBUTE_TOKEN_PREFIX); CAS_ATTRIBUTE_TOKEN_PREFIX);
Object value = attr.getValue(); Object value = attr.getValue();
if (value != null) if (value != null) {
tokens.put(tokenName, value.toString()); String attrValue = value.toString();
tokens.put(tokenName, attrValue);
if (attr.getKey().equals(groupAttribute)) {
Matcher matcher =
pattern.matcher(attrValue.substring(1,attrValue.length()-1));
while (matcher.find()) {
effectiveGroups.add(matcher.group(1));
}
}
}
} }
return tokens; CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(username, credentials, tokens, effectiveGroups);
return authenticatedUser;
} }
catch (TicketValidationException e) { catch (TicketValidationException e) {
throw new GuacamoleException("Ticket validation failed.", e); throw new GuacamoleException("Ticket validation failed.", e);

View File

@@ -22,6 +22,7 @@ package org.apache.guacamole.auth.cas.user;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
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;
@@ -50,6 +51,11 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
*/ */
private Map<String, String> tokens; private Map<String, String> tokens;
/**
* The unique identifiers of all user groups which this user is a member of.
*/
private Set<String> effectiveGroups;
/** /**
* Initializes this AuthenticatedUser using the given username and * Initializes this AuthenticatedUser using the given username and
* credentials, and an empty map of parameter tokens. * credentials, and an empty map of parameter tokens.
@@ -61,7 +67,7 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
* The credentials provided when this user was authenticated. * The credentials provided when this user was authenticated.
*/ */
public void init(String username, Credentials credentials) { public void init(String username, Credentials credentials) {
this.init(username, credentials, Collections.emptyMap()); this.init(username, credentials, Collections.emptyMap(), Collections.emptySet());
} }
/** /**
@@ -79,9 +85,10 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
* as tokens when connections are established with this user. * as tokens when connections are established with this user.
*/ */
public void init(String username, Credentials credentials, public void init(String username, Credentials credentials,
Map<String, String> tokens) { Map<String, String> tokens, Set<String> effectiveGroups) {
this.credentials = credentials; this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens); this.tokens = Collections.unmodifiableMap(tokens);
this.effectiveGroups = effectiveGroups;
setIdentifier(username.toLowerCase()); setIdentifier(username.toLowerCase());
} }
@@ -107,4 +114,9 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
return credentials; return credentials;
} }
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
} }