diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index f4b30e4b4..2ffe8182e 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -183,6 +183,26 @@
provided
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.6.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.6.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.6.0
+ test
+
+
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 000000000..4e315daf4
--- /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 000000000..5c31d5db4
--- /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 000000000..9a33ef738
--- /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 000000000..04c6c0588
--- /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/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 000000000..ffff0a750
--- /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"));
+
+ }
+
+}