Merge 1.3.0 changes back to master.
diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index f4b30e4..2ffe818 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -183,6 +183,26 @@
<scope>provided</scope>
</dependency>
+ <!-- JUnit -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>5.6.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <version>5.6.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.6.0</version>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
</project>
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 9f171e8..8150f97 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,7 @@
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 javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.GuacamoleException;
@@ -54,12 +52,6 @@
private TicketValidationService ticketService;
/**
- * Provider for AuthenticatedUser objects.
- */
- @Inject
- private Provider<CASAuthenticatedUser> authenticatedUserProvider;
-
- /**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
@@ -82,13 +74,7 @@
if (request != null) {
String ticket = request.getParameter(CASTicketField.PARAMETER_NAME);
if (ticket != null) {
- Map<String, String> 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 2ee42db..7bb363f 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
@@ -19,7 +19,10 @@
package org.apache.guacamole.auth.cas.conf;
+import org.apache.guacamole.auth.cas.group.GroupFormat;
+import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.apache.guacamole.properties.URIGuacamoleProperty;
+import org.apache.guacamole.properties.StringGuacamoleProperty;
/**
* Provides properties required for use of the CAS authentication provider.
@@ -68,5 +71,51 @@
public String getName() { return "cas-clearpass-key"; }
};
+
+ /**
+ * The name of the CAS attribute used for group membership, such as
+ * "memberOf". This attribute is case sensitive.
+ */
+ public static final StringGuacamoleProperty CAS_GROUP_ATTRIBUTE =
+ new StringGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "cas-group-attribute"; }
+
+ };
+
+ /**
+ * The format used by CAS to represent group names. Possible formats are
+ * "plain" (simple text names) or "ldap" (fully-qualified LDAP DNs).
+ */
+ public static final EnumGuacamoleProperty<GroupFormat> CAS_GROUP_FORMAT =
+ new EnumGuacamoleProperty<GroupFormat>(GroupFormat.class) {
+
+ @Override
+ public String getName() { return "cas-group-format"; }
+
+ };
+
+ /**
+ * The LDAP base DN to require for all CAS groups.
+ */
+ public static final LdapNameGuacamoleProperty CAS_GROUP_LDAP_BASE_DN =
+ new LdapNameGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "cas-group-ldap-base-dn"; }
+
+ };
+
+ /**
+ * The LDAP attribute to require for the names of CAS groups.
+ */
+ public static final StringGuacamoleProperty CAS_GROUP_LDAP_ATTRIBUTE =
+ new StringGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "cas-group-ldap-attribute"; }
+
+ };
}
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 680f170..ce5edd8 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
@@ -22,8 +22,14 @@
import com.google.inject.Inject;
import java.net.URI;
import java.security.PrivateKey;
+import javax.naming.ldap.LdapName;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.cas.group.GroupFormat;
import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.auth.cas.group.GroupParser;
+import org.apache.guacamole.auth.cas.group.LDAPGroupParser;
+import org.apache.guacamole.auth.cas.group.PlainGroupParser;
/**
* Service for retrieving configuration information regarding the CAS service.
@@ -85,4 +91,102 @@
return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY);
}
+ /**
+ * Returns the CAS attribute that should be used to determine group
+ * memberships in CAS, such as "memberOf". If no attribute has been
+ * specified, null is returned.
+ *
+ * @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 format that CAS is expected to use for its group names, such
+ * as {@link GroupFormat#PLAIN} (simple plain-text names) or
+ * {@link GroupFormat#LDAP} (fully-qualified LDAP DNs). If not specified,
+ * PLAIN is used by default.
+ *
+ * @return
+ * The format that CAS is expected to use for its group names.
+ *
+ * @throws GuacamoleException
+ * If the format specified within guacamole.properties is not valid.
+ */
+ public GroupFormat getGroupFormat() throws GuacamoleException {
+ return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_FORMAT, GroupFormat.PLAIN);
+ }
+
+ /**
+ * Returns the base DN that all LDAP-formatted CAS groups must reside
+ * beneath. Any groups that are not beneath this base DN should be ignored.
+ * If no such base DN is provided, the tree structure of the ancestors of
+ * LDAP-formatted CAS groups should not be considered.
+ *
+ * @return
+ * The base DN that all LDAP-formatted CAS groups must reside beneath,
+ * or null if the tree structure of the ancestors of LDAP-formatted
+ * CAS groups should not be considered.
+ *
+ * @throws GuacamoleException
+ * If the provided base DN is not a valid LDAP DN.
+ */
+ public LdapName getGroupLDAPBaseDN() throws GuacamoleException {
+ return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_BASE_DN);
+ }
+
+ /**
+ * Returns the LDAP attribute that should be required for all LDAP-formatted
+ * CAS groups. Any groups that do not use this attribute as the last
+ * (leftmost) attribute of their DN should be ignored. If no such LDAP
+ * attribute is provided, the last (leftmost) attribute should still be
+ * used to determine the group name, but the specific attribute involved
+ * should not be considered.
+ *
+ * @return
+ * The LDAP attribute that should be required for all LDAP-formatted
+ * CAS groups, or null if any attribute should be allowed.
+ *
+ * @throws GuacamoleException
+ * If guacamole.properties cannot be parsed.
+ */
+ public String getGroupLDAPAttribute() throws GuacamoleException {
+ return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_ATTRIBUTE);
+ }
+
+ /**
+ * Returns a GroupParser instance that can be used to parse CAS group
+ * names. The parser returned will take into account the configured CAS
+ * group format, as well as any configured LDAP-specific restrictions.
+ *
+ * @return
+ * A GroupParser instance that can be used to parse CAS group names as
+ * configured in guacamole.properties.
+ *
+ * @throws GuacamoleException
+ * If guacamole.properties cannot be parsed.
+ */
+ public GroupParser getGroupParser() throws GuacamoleException {
+ switch (getGroupFormat()) {
+
+ // Simple, plain-text groups
+ case PLAIN:
+ return new PlainGroupParser();
+
+ // LDAP DNs
+ case LDAP:
+ return new LDAPGroupParser(getGroupLDAPAttribute(), getGroupLDAPBaseDN());
+
+ default:
+ throw new GuacamoleServerException("Unsupported CAS group format: " + getGroupFormat());
+
+ }
+ }
+
}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java
new file mode 100644
index 0000000..5469376
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java
@@ -0,0 +1,49 @@
+/*
+ * 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.cas.conf;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import org.apache.guacamole.properties.GuacamoleProperty;
+import org.apache.guacamole.GuacamoleServerException;
+
+/**
+ * A GuacamoleProperty whose value is an LDAP DN.
+ */
+public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName> {
+
+ @Override
+ public LdapName parseValue(String value) throws GuacamoleServerException {
+
+ // Consider null/empty values to be empty
+ if (value == null || value.isEmpty())
+ return null;
+
+ // Parse provided value as an LDAP DN
+ try {
+ return new LdapName(value);
+ }
+ catch (InvalidNameException e) {
+ throw new GuacamoleServerException("Invalid LDAP distinguished name.", e);
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java
new file mode 100644
index 0000000..4e315da
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java
@@ -0,0 +1,41 @@
+/*
+ * 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.cas.group;
+
+import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
+
+/**
+ * Possible formats of group names received from CAS.
+ */
+public enum GroupFormat {
+
+ /**
+ * Simple, plain-text group names.
+ */
+ @PropertyValue("plain")
+ PLAIN,
+
+ /**
+ * Group names formatted as LDAP DNs.
+ */
+ @PropertyValue("ldap")
+ LDAP
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java
new file mode 100644
index 0000000..5c31d5d
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java
@@ -0,0 +1,44 @@
+/*
+ * 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.cas.group;
+
+/**
+ * Parser which converts the group names returned by CAS into names usable by
+ * Guacamole. The format of a CAS group name may vary by the underlying
+ * authentication backend. For example, a CAS deployment backed by LDAP may
+ * provide group names as LDAP DNs, which must be transformed into normal group
+ * names to be usable within Guacamole.
+ *
+ * @see LDAPGroupParser
+ */
+public interface GroupParser {
+
+ /**
+ * Parses the given CAS group name into a group name usable by Guacamole.
+ *
+ * @param casGroup
+ * The group name retrieved from CAS.
+ *
+ * @return
+ * A group name usable by Guacamole, or null if the group is not valid.
+ */
+ String parse(String casGroup);
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java
new file mode 100644
index 0000000..9a33ef7
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cas.group;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GroupParser that converts group names from LDAP DNs into normal group names,
+ * using the last (leftmost) attribute of the DN as the name. Groups may
+ * optionally be restricted to only those beneath a specific base DN, or only
+ * those using a specific attribute as their last (leftmost) attribute.
+ */
+public class LDAPGroupParser implements GroupParser {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(LDAPGroupParser.class);
+
+ /**
+ * The LDAP attribute to require for all accepted group names. If null, any
+ * LDAP attribute will be allowed.
+ */
+ private final String nameAttribute;
+
+ /**
+ * The base DN to require for all accepted group names. If null, ancestor
+ * tree structure will not be considered in accepting/rejecting a group.
+ */
+ private final LdapName baseDn;
+
+ /**
+ * Creates a new LDAPGroupParser which applies the given restrictions on
+ * any provided group names.
+ *
+ * @param nameAttribute
+ * The LDAP attribute to require for all accepted group names. This
+ * restriction applies to the last (leftmost) attribute only, which is
+ * always used to determine the name of the group. If null, any LDAP
+ * attribute will be allowed in the last (leftmost) position.
+ *
+ * @param baseDn
+ * The base DN to require for all accepted group names. If null,
+ * ancestor tree structure will not be considered in
+ * accepting/rejecting a group.
+ */
+ public LDAPGroupParser(String nameAttribute, LdapName baseDn) {
+ this.nameAttribute = nameAttribute;
+ this.baseDn = baseDn;
+ }
+
+ @Override
+ public String parse(String casGroup) {
+
+ // Reject null/empty group names
+ if (casGroup == null || casGroup.isEmpty())
+ return null;
+
+ // Parse group as an LDAP DN
+ LdapName group;
+ try {
+ group = new LdapName(casGroup);
+ }
+ catch (InvalidNameException e) {
+ logger.debug("CAS group \"{}\" has been rejected as it is not a "
+ + "valid LDAP DN.", casGroup, e);
+ return null;
+ }
+
+ // Reject any group that is not beneath the base DN
+ if (baseDn != null && !group.startsWith(baseDn))
+ return null;
+
+ // If a specific name attribute is defined, restrict to groups that
+ // use that attribute to distinguish themselves
+ Rdn last = group.getRdn(group.size() - 1);
+ if (nameAttribute != null && !nameAttribute.equalsIgnoreCase(last.getType()))
+ return null;
+
+ // The group name is the string value of the final attribute in the DN
+ return last.getValue().toString();
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java
new file mode 100644
index 0000000..04c6c05
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cas.group;
+
+/**
+ * GroupParser which simply passes through all CAS group names untouched.
+ */
+public class PlainGroupParser implements GroupParser {
+
+ @Override
+ public String parse(String casGroup) {
+ return casGroup;
+ }
+
+}
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 fce4760..17ef923 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 @@
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;
@@ -30,13 +31,17 @@
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
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;
@@ -69,6 +74,46 @@
private ConfigurationService confService;
/**
+ * Provider for AuthenticatedUser objects.
+ */
+ @Inject
+ private Provider<CASAuthenticatedUser> authenticatedUserProvider;
+
+ /**
+ * Converts the given CAS attribute value object (whose type is variable)
+ * to a Set of String values. If the value is already a Collection of some
+ * kind, its values are converted to Strings and returned as the members of
+ * the Set. If the value is not already a Collection, it is assumed to be a
+ * single value, converted to a String, and used as the sole member of the
+ * set.
+ *
+ * @param obj
+ * The CAS attribute value to convert to a Set of Strings.
+ *
+ * @return
+ * A Set of all String values contained within the given CAS attribute
+ * value.
+ */
+ private Set<String> toStringSet(Object obj) {
+
+ // Consider null to represent no provided values
+ if (obj == null)
+ return Collections.emptySet();
+
+ // If the provided object is already a Collection, produce a Collection
+ // where we know for certain that all values are Strings
+ if (obj instanceof Collection) {
+ return ((Collection<?>) obj).stream()
+ .map(Object::toString)
+ .collect(Collectors.toSet());
+ }
+
+ // Otherwise, assume we have only a single value
+ return Collections.singleton(obj.toString());
+
+ }
+
+ /**
* Validates and parses the given ID ticket, returning a map of all
* available tokens for the given user based on attributes provided by the
* CAS server. If the ticket is invalid an exception is thrown.
@@ -81,62 +126,80 @@
* 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<String, String> validateTicket(String ticket,
+ public CASAuthenticatedUser validateTicket(String ticket,
Credentials credentials) throws GuacamoleException {
- // Retrieve the configured CAS URL, establish a ticket validator,
- // and then attempt to validate the supplied ticket. If that succeeds,
- // grab the principal returned by the validator.
+ // Create a ticket validator that uses the configured CAS URL
URI casServerUrl = confService.getAuthorizationEndpoint();
Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString());
validator.setAcceptAnyProxy(true);
validator.setEncoding("UTF-8");
+
+ // Attempt to validate the supplied ticket
+ Assertion assertion;
try {
- Map<String, String> tokens = new HashMap<>();
URI confRedirectURI = confService.getRedirectURI();
- Assertion a = validator.validate(ticket, confRedirectURI.toString());
- AttributePrincipal principal = a.getPrincipal();
- Map<String, Object> ticketAttrs =
- new HashMap<>(principal.getAttributes());
-
- // Retrieve username and set the credentials.
- String username = principal.getName();
- if (username == null)
- throw new GuacamoleSecurityException("No username provided by CAS.");
-
- credentials.setUsername(username);
-
- // Retrieve password, attempt decryption, and set credentials.
- Object credObj = ticketAttrs.remove("credential");
- if (credObj != null) {
- String clearPass = decryptPassword(credObj.toString());
- if (clearPass != null && !clearPass.isEmpty())
- credentials.setPassword(clearPass);
- }
-
- // Convert remaining attributes that have values to Strings
- for (Entry <String, Object> 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());
- }
-
- return tokens;
-
- }
+ assertion = validator.validate(ticket, confRedirectURI.toString());
+ }
catch (TicketValidationException e) {
throw new GuacamoleException("Ticket validation failed.", e);
}
+ // Pull user principal and associated attributes
+ AttributePrincipal principal = assertion.getPrincipal();
+ Map<String, Object> ticketAttrs = new HashMap<>(principal.getAttributes());
+
+ // Retrieve user identity from principal
+ String username = principal.getName();
+ if (username == null)
+ throw new GuacamoleSecurityException("No username provided by CAS.");
+
+ // Update credentials with username provided by CAS for sake of
+ // ${GUAC_USERNAME} token
+ credentials.setUsername(username);
+
+ // Retrieve password, attempt decryption, and set credentials.
+ Object credObj = ticketAttrs.remove("credential");
+ if (credObj != null) {
+ String clearPass = decryptPassword(credObj.toString());
+ if (clearPass != null && !clearPass.isEmpty())
+ credentials.setPassword(clearPass);
+ }
+
+ Set<String> effectiveGroups;
+
+ // Parse effective groups from principal attributes if a specific
+ // group attribute has been configured
+ String groupAttribute = confService.getGroupAttribute();
+ if (groupAttribute != null) {
+ effectiveGroups = toStringSet(ticketAttrs.get(groupAttribute)).stream()
+ .map(confService.getGroupParser()::parse)
+ .collect(Collectors.toSet());
+ }
+
+ // Otherwise, assume no effective groups
+ else
+ effectiveGroups = Collections.emptySet();
+
+ // Convert remaining attributes that have values to Strings
+ Map<String, String> tokens = new HashMap<>(ticketAttrs.size());
+ ticketAttrs.forEach((key, value) -> {
+ if (value != null) {
+ String tokenName = TokenName.canonicalize(key, CAS_ATTRIBUTE_TOKEN_PREFIX);
+ tokens.put(tokenName, value.toString());
+ }
+ });
+
+ CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+ authenticatedUser.init(username, credentials, tokens, effectiveGroups);
+ return authenticatedUser;
+
}
/**
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 1b3a948..b79344e 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 @@
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;
@@ -51,6 +52,11 @@
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
* credentials, and an empty map of parameter tokens.
*
@@ -61,7 +67,7 @@
* 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 @@
* as tokens when connections are established with this user.
*/
public void init(String username, Credentials credentials,
- Map<String, String> tokens) {
+ Map<String, String> tokens, Set<String> effectiveGroups) {
this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens);
+ this.effectiveGroups = effectiveGroups;
setIdentifier(username.toLowerCase());
}
@@ -107,4 +114,9 @@
return credentials;
}
+ @Override
+ public Set<String> getEffectiveUserGroups() {
+ return effectiveGroups;
+ }
+
}
diff --git a/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java b/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java
new file mode 100644
index 0000000..ffff0a7
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.cas.group;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test which confirms that the LDAPGroupParser implementation of GroupParser
+ * parses CAS groups correctly.
+ */
+public class LDAPGroupParserTest {
+
+ /**
+ * LdapName instance representing the LDAP DN: "dc=example,dc=net".
+ */
+ private final LdapName exampleBaseDn;
+
+ /**
+ * Creates a new LDAPGroupParserTest that verifies the functionality of
+ * LDAPGroupParser.
+ *
+ * @throws InvalidNameException
+ * If the static string LDAP DN of any test instance of LdapName is
+ * unexpectedly invalid.
+ */
+ public LDAPGroupParserTest() throws InvalidNameException {
+ exampleBaseDn = new LdapName("dc=example,dc=net");
+ }
+
+ /**
+ * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+ * when no restrictions are enforced on LDAP attributes or the base DN.
+ */
+ @Test
+ public void testParseRestrictNothing() {
+
+ GroupParser parser = new LDAPGroupParser(null, null);
+
+ // null input should be rejected as null
+ assertNull(parser.parse(null));
+
+ // Invalid DNs should be rejected as null
+ assertNull(parser.parse(""));
+ assertNull(parser.parse("foo"));
+
+ // Valid DNs should be accepted
+ assertEquals("bar", parser.parse("foo=bar"));
+ assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
+ assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
+ assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+ assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+ assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+
+ }
+
+ /**
+ * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+ * when restrictions are enforced on LDAP attributes only.
+ */
+ @Test
+ public void testParseRestrictAttribute() {
+
+ GroupParser parser = new LDAPGroupParser("cn", null);
+
+ // null input should be rejected as null
+ assertNull(parser.parse(null));
+
+ // Invalid DNs should be rejected as null
+ assertNull(parser.parse(""));
+ assertNull(parser.parse("foo"));
+
+ // Valid DNs not using the correct attribute should be rejected as null
+ assertNull(parser.parse("foo=bar"));
+ assertNull(parser.parse("ou=baz,dc=example,dc=com"));
+ assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=com"));
+
+ // Valid DNs using the correct attribute should be accepted
+ assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+ assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+ assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
+
+ }
+
+ /**
+ * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+ * when restrictions are enforced on the LDAP base DN only.
+ */
+ @Test
+ public void testParseRestrictBaseDN() {
+
+ GroupParser parser = new LDAPGroupParser(null, exampleBaseDn);
+
+ // null input should be rejected as null
+ assertNull(parser.parse(null));
+
+ // Invalid DNs should be rejected as null
+ assertNull(parser.parse(""));
+ assertNull(parser.parse("foo"));
+
+ // Valid DNs outside the base DN should be rejected as null
+ assertNull(parser.parse("foo=bar"));
+ assertNull(parser.parse("CN=baz,dc=example,dc=com"));
+
+ // Valid DNs beneath the base DN should be accepted
+ assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+ assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+ assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
+ assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+
+ }
+
+ /**
+ * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+ * when restrictions are enforced on both LDAP attributes and the base DN.
+ */
+ @Test
+ public void testParseRestrictAll() {
+
+ GroupParser parser = new LDAPGroupParser("cn", exampleBaseDn);
+
+ // null input should be rejected as null
+ assertNull(parser.parse(null));
+
+ // Invalid DNs should be rejected as null
+ assertNull(parser.parse(""));
+ assertNull(parser.parse("foo"));
+
+ // Valid DNs outside the base DN should be rejected as null
+ assertNull(parser.parse("foo=bar"));
+ assertNull(parser.parse("CN=baz,dc=example,dc=com"));
+
+ // Valid DNs beneath the base DN but not using the correct attribute
+ // should be rejected as null
+ assertNull(parser.parse("ou=baz,dc=example,dc=net"));
+ assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+
+ // Valid DNs beneath the base DN and using the correct attribute should
+ // be accepted
+ assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+ assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+
+ }
+
+}
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 5123a8a..e58ba9e 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -678,6 +678,10 @@
set_property "cas-authorization-endpoint" "$CAS_AUTHORIZATION_ENDPOINT"
set_property "cas-redirect-uri" "$CAS_REDIRECT_URI"
set_optional_property "cas-clearpass-key" "$CAS_CLEARPASS_KEY"
+ set_optional_property "cas-group-attribute" "$CAS_GROUP_ATTRIBUTE"
+ set_optional_property "cas-group-format" "$CAS_GROUP_FORMAT"
+ set_optional_property "cas-group-ldap-base-dn" "$CAS_GROUP_LDAP_BASE_DN"
+ set_optional_property "cas-group-ldap-attribute" "$CAS_GROUP_LDAP_ATTRIBUTE"
# Add required .jar files to GUACAMOLE_EXT
ln -s /opt/guacamole/cas/guacamole-auth-*.jar "$GUACAMOLE_EXT"