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")); + + } + +}