GUACAMOLE-793: Merge add support for retrieving effective groups from CAS.

This commit is contained in:
Virtually Nick
2020-12-14 07:09:40 -05:00
committed by GitHub
13 changed files with 731 additions and 57 deletions

View File

@@ -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>

View File

@@ -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;
@@ -53,12 +51,6 @@ public class AuthenticationProviderService {
@Inject
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 @@ public class AuthenticationProviderService {
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);
}
}

View File

@@ -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 class CASGuacamoleProperties {
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"; }
};
}

View File

@@ -22,8 +22,14 @@ package org.apache.guacamole.auth.cas.conf;
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 @@ public class ConfigurationService {
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());
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
@@ -30,13 +31,17 @@ import javax.crypto.Cipher;
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;
@@ -68,6 +73,46 @@ public class TicketValidationService {
@Inject
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
@@ -81,62 +126,80 @@ 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<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;
}
/**

View File

@@ -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<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 @@ 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<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 @@ public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
}

View File

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

View File

@@ -678,6 +678,10 @@ END
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"