From 7b8dc36644be90f5ddb90e4d7835e062475421ea Mon Sep 17 00:00:00 2001 From: Ron Record Date: Sat, 5 Dec 2020 22:09:45 -0800 Subject: [PATCH] GUACAMOLE-793: validateTicket() returns the CASAuthenticatedUser instance rather than just a token so CAS Provider can return Group - like LDAP Provider --- .../cas/AuthenticationProviderService.java | 23 ++++---- .../auth/cas/conf/CASGuacamoleProperties.java | 24 +++++++++ .../auth/cas/conf/ConfigurationService.java | 36 +++++++++++++ .../cas/ticket/TicketValidationService.java | 54 ++++++++++++++++--- .../auth/cas/user/CASAuthenticatedUser.java | 16 +++++- 5 files changed, 130 insertions(+), 23 deletions(-) diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java index 9f171e8ac..6fb400264 100644 --- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java @@ -20,9 +20,9 @@ package org.apache.guacamole.auth.cas; import com.google.inject.Inject; -import com.google.inject.Provider; import java.util.Arrays; import java.util.Map; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.form.Field; 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.user.CASAuthenticatedUser; import org.apache.guacamole.language.TranslatableMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Service providing convenience functions for the CAS AuthenticationProvider @@ -41,6 +43,11 @@ import org.apache.guacamole.language.TranslatableMessage; */ public class AuthenticationProviderService { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class); + /** * Service for retrieving CAS configuration information. */ @@ -53,12 +60,6 @@ public class AuthenticationProviderService { @Inject private TicketValidationService ticketService; - /** - * Provider for AuthenticatedUser objects. - */ - @Inject - private Provider authenticatedUserProvider; - /** * Returns an AuthenticatedUser representing the user authenticated by the * given credentials. @@ -82,13 +83,7 @@ public class AuthenticationProviderService { if (request != null) { String ticket = request.getParameter(CASTicketField.PARAMETER_NAME); if (ticket != null) { - Map tokens = ticketService.validateTicket(ticket, credentials); - String username = credentials.getUsername(); - if (username != null) { - CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(username, credentials, tokens); - return authenticatedUser; - } + return ticketService.validateTicket(ticket, credentials); } } diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java index 2ee42dba9..77c7667e5 100644 --- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java +++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.cas.conf; import org.apache.guacamole.properties.URIGuacamoleProperty; +import org.apache.guacamole.properties.StringGuacamoleProperty; /** * Provides properties required for use of the CAS authentication provider. @@ -68,5 +69,28 @@ public class CASGuacamoleProperties { public String getName() { return "cas-clearpass-key"; } }; + + /** + * 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"; } + + }; } diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java index 680f17057..1285c619c 100644 --- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java @@ -85,4 +85,40 @@ public class ConfigurationService { 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); + } + + + } diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java index fce476040..24c3e24a8 100644 --- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java +++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java @@ -21,6 +21,7 @@ package org.apache.guacamole.auth.cas.ticket; import com.google.common.io.BaseEncoding; import com.google.inject.Inject; +import com.google.inject.Provider; import java.net.URI; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -33,10 +34,15 @@ import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; 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.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleServerException; 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.token.TokenName; import org.jasig.cas.client.authentication.AttributePrincipal; @@ -68,6 +74,12 @@ public class TicketValidationService { @Inject private ConfigurationService confService; + /** + * Provider for AuthenticatedUser objects. + */ + @Inject + private Provider authenticatedUserProvider; + /** * Validates and parses the given ID ticket, returning a map of all * available tokens for the given user based on attributes provided by the @@ -81,14 +93,13 @@ public class TicketValidationService { * password values in. * * @return - * A Map all of tokens for the user parsed from attributes returned - * by the CAS server. + * A CASAuthenticatedUser instance containing the ticket data returned by the CAS server. * * @throws GuacamoleException * If the ID ticket is not valid or guacamole.properties could * not be parsed. */ - public Map validateTicket(String ticket, + public CASAuthenticatedUser validateTicket(String ticket, Credentials credentials) throws GuacamoleException { // Retrieve the configured CAS URL, establish a ticket validator, @@ -100,6 +111,7 @@ public class TicketValidationService { validator.setEncoding("UTF-8"); try { Map tokens = new HashMap<>(); + Set effectiveGroups = new HashSet<>(); URI confRedirectURI = confService.getRedirectURI(); Assertion a = validator.validate(ticket, confRedirectURI.toString()); AttributePrincipal principal = a.getPrincipal(); @@ -122,16 +134,44 @@ public class TicketValidationService { } // 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 attr : ticketAttrs.entrySet()) { String tokenName = TokenName.canonicalize(attr.getKey(), CAS_ATTRIBUTE_TOKEN_PREFIX); Object value = attr.getValue(); - if (value != null) - tokens.put(tokenName, value.toString()); + if (value != null) { + 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) { throw new GuacamoleException("Ticket validation failed.", e); diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java index 1b3a948cc..b79344eb8 100644 --- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java +++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java @@ -22,6 +22,7 @@ package org.apache.guacamole.auth.cas.user; import com.google.inject.Inject; import java.util.Collections; import java.util.Map; +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; @@ -50,6 +51,11 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser { */ private Map tokens; + /** + * The unique identifiers of all user groups which this user is a member of. + */ + private Set effectiveGroups; + /** * Initializes this AuthenticatedUser using the given username and * 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. */ 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. */ public void init(String username, Credentials credentials, - Map tokens) { + Map tokens, Set effectiveGroups) { this.credentials = credentials; this.tokens = Collections.unmodifiableMap(tokens); + this.effectiveGroups = effectiveGroups; setIdentifier(username.toLowerCase()); } @@ -107,4 +114,9 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser { return credentials; } + @Override + public Set getEffectiveUserGroups() { + return effectiveGroups; + } + }