From ff75376dcfb8ea501d2bf1d7b6ece03bcc0226b3 Mon Sep 17 00:00:00 2001 From: Anthony Bucci Date: Thu, 20 Mar 2025 14:58:33 -0400 Subject: [PATCH] SAML SSO unit tests etc. (#455) * Added two unit tests to SecurityUtilsTest to verify the found email address is correct * Removed unused imports in SecurityUtilsTest * Added unit tests for ServicesConfigReader while investigating DRYD-1702 * cleaned up leftover printlns in SecurityUtilsTest * cleaned up imports in SecurityUtilsTest * reorganized methods in ServicesConfigReaderImplTest * refactored findUser method to make it easier to test and prepare it for deprecation of ReponseToken * refactored some useful common code out of SecurityUtilsTest into AbstractSecurityTestBase * refactored some of the SAML-object-creating utility methods * made 'parse' methods public so they can be tested * added unit test to check that 'identifier' probe assertions correctly pull out attribute values * source cleanup: organizing imports, formatting * added .mvn to .gitignore to ignore local, per-developer maven properties --------- Co-authored-by: Anthony Bucci --- .gitignore | 1 + ...eSaml2ResponseAuthenticationConverter.java | 255 ++++++++------- .../common/test/AbstractSecurityTestBase.java | 209 ++++++++++++ .../common/test/SecurityUtilsTest.java | 303 ++++++------------ services/config/pom.xml | 11 + .../config/AbstractConfigReaderImpl.java | 4 +- .../config/ServicesConfigReaderImplTest.java | 139 ++++++++ 7 files changed, 601 insertions(+), 321 deletions(-) create mode 100644 services/common/src/test/java/org/collectionspace/services/common/test/AbstractSecurityTestBase.java create mode 100644 services/config/src/test/java/org/collectionspace/services/common/config/ServicesConfigReaderImplTest.java diff --git a/.gitignore b/.gitignore index db5b3c75a..f1b47dc39 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ bin *.diff logged_schemas logs +.mvn diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/CSpaceSaml2ResponseAuthenticationConverter.java b/services/common/src/main/java/org/collectionspace/services/common/security/CSpaceSaml2ResponseAuthenticationConverter.java index 0a2aa3564..d91bae9ca 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/security/CSpaceSaml2ResponseAuthenticationConverter.java +++ b/services/common/src/main/java/org/collectionspace/services/common/security/CSpaceSaml2ResponseAuthenticationConverter.java @@ -22,124 +22,139 @@ import org.springframework.security.saml2.provider.service.authentication.OpenSa import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; public class CSpaceSaml2ResponseAuthenticationConverter implements Converter { - private final Logger logger = LoggerFactory.getLogger(CSpaceSaml2ResponseAuthenticationConverter.class); - - private CSpaceUserDetailsService userDetailsService; - - public CSpaceSaml2ResponseAuthenticationConverter(CSpaceUserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; - } - - @Override - public CSpaceSaml2Authentication convert(ResponseToken responseToken) { - Saml2Authentication authentication = OpenSamlAuthenticationProvider - .createDefaultResponseAuthenticationConverter() - .convert(responseToken); - - String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId(); - ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); - SAMLRelyingPartyType relyingPartyRegistration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig, registrationId); - CSpaceUser user = findUser(relyingPartyRegistration, responseToken); - - if (user != null) { - return new CSpaceSaml2Authentication(user, authentication); - } - - return null; - } - - /** - * Attempt to find a CSpace user for a SAML response. - * - * @param relyingPartyRegistration - * @param responseToken - * @return - */ - private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, ResponseToken responseToken) { - AssertionProbesType assertionSsoIdProbes = ( - relyingPartyRegistration != null - ? relyingPartyRegistration.getAssertionSsoIdProbes() - : null - ); - - AssertionProbesType assertionUsernameProbes = ( - relyingPartyRegistration != null - ? relyingPartyRegistration.getAssertionUsernameProbes() - : null - ); - - List attemptedUsernames = new ArrayList<>(); - List assertions = responseToken.getResponse().getAssertions(); - - SecurityUtils.logSamlAssertions(assertions); - - for (Assertion assertion : assertions) { - CSpaceUser user = null; - String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes); - - // First, look for a CSpace user whose SSO ID is the ID in the assertion. - - if (ssoId != null) { - try { - user = (CSpaceUser) userDetailsService.loadUserBySsoId(ssoId); - } - catch (UsernameNotFoundException e) { - } - } - - if (user != null) { - return user; - } - - // Next, look for a CSpace user whose username is the email address in the assertion. - - Set candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion, assertionUsernameProbes); - - for (String candidateUsername : candidateUsernames) { - try { - user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername); - - if (user != null) { - String expectedSsoId = user.getSsoId(); - - if (expectedSsoId == null) { - // Store the ID from the IdP to use in future log ins. Note that this does not save - // the SSO ID to the database. That happens in CSpaceAuthenticationSuccessEvent. - - user.setSsoId(ssoId); - - // TODO: If the email address in the assertion differs from the CSpace user's email, - // update the CSpace user. - } else if (!StringUtils.equals(expectedSsoId, ssoId)) { - // If the user previously logged in via SSO, but they had a different ID from the - // IdP, something's wrong. (Did an account on the IdP get assigned an email that - // previously belonged to a different account on the IdP?) - - logger.warn("User with username {} has expected SSO ID {}, but received {} in SAML assertion", - candidateUsername, expectedSsoId, ssoId); - - user = null; - } - - if (user != null) { - return user; - } - } - } - catch(UsernameNotFoundException e) { - } - } - - attemptedUsernames.addAll(candidateUsernames); - } - - // No CSpace user was found for this SAML response. - // TODO: Auto-create a CSpace user, using the display name, email address, and ID in the response. - - String errorMessage = attemptedUsernames.size() == 0 - ? "The SAML response did not contain a CollectionSpace username." - : "No CollectionSpace account found for " + StringUtils.join(attemptedUsernames, " / ") + "."; - - throw(new UsernameNotFoundException(errorMessage)); - } + private final Logger logger = LoggerFactory.getLogger(CSpaceSaml2ResponseAuthenticationConverter.class); + + private CSpaceUserDetailsService userDetailsService; + + public CSpaceSaml2ResponseAuthenticationConverter(CSpaceUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public CSpaceSaml2Authentication convert(ResponseToken responseToken) { + Saml2Authentication authentication = OpenSamlAuthenticationProvider + .createDefaultResponseAuthenticationConverter().convert(responseToken); + + String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId(); + ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig(); + SAMLRelyingPartyType relyingPartyRegistration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig, + registrationId); + CSpaceUser user = findUser(relyingPartyRegistration, responseToken); + + if (user != null) { + return new CSpaceSaml2Authentication(user, authentication); + } + + return null; + } + + /** + * Attempt to find a CSpace user for from a list of relying parties + * (from the services configuration) and a list of assertions (from the SAML response) + * + * @param relyingPartyRegistration object representing relying parties from the + * service config + * @param assertions list of SAML assertions to test + * @return the found CSpace user, if any + */ + private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, List assertions) { + AssertionProbesType assertionSsoIdProbes = (relyingPartyRegistration != null + ? relyingPartyRegistration.getAssertionSsoIdProbes() + : null); + + AssertionProbesType assertionUsernameProbes = (relyingPartyRegistration != null + ? relyingPartyRegistration.getAssertionUsernameProbes() + : null); + + List attemptedUsernames = new ArrayList<>(); + + SecurityUtils.logSamlAssertions(assertions); + + for (Assertion assertion : assertions) { + CSpaceUser user = null; + String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes); + + // First, look for a CSpace user whose SSO ID is the ID in the assertion. + + if (ssoId != null) { + try { + user = (CSpaceUser) userDetailsService.loadUserBySsoId(ssoId); + } catch (UsernameNotFoundException e) { + } + } + + if (user != null) { + return user; + } + + // Next, look for a CSpace user whose username is the email address in the + // assertion. + + Set candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion, + assertionUsernameProbes); + + for (String candidateUsername : candidateUsernames) { + try { + user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername); + + if (user != null) { + String expectedSsoId = user.getSsoId(); + + if (expectedSsoId == null) { + // Store the ID from the IdP to use in future log ins. Note that this does not + // save + // the SSO ID to the database. That happens in CSpaceAuthenticationSuccessEvent. + + user.setSsoId(ssoId); + + // TODO: If the email address in the assertion differs from the CSpace user's + // email, + // update the CSpace user. + } else if (!StringUtils.equals(expectedSsoId, ssoId)) { + // If the user previously logged in via SSO, but they had a different ID from + // the + // IdP, something's wrong. (Did an account on the IdP get assigned an email that + // previously belonged to a different account on the IdP?) + + logger.warn( + "User with username {} has expected SSO ID {}, but received {} in SAML assertion", + candidateUsername, expectedSsoId, ssoId); + + user = null; + } + + if (user != null) { + return user; + } + } + } catch (UsernameNotFoundException e) { + } + } + + attemptedUsernames.addAll(candidateUsernames); + } + + // No CSpace user was found for this SAML response. + // TODO: Auto-create a CSpace user, using the display name, email address, and + // ID in the response. + + String errorMessage = attemptedUsernames.size() == 0 + ? "The SAML response did not contain a CollectionSpace username." + : "No CollectionSpace account found for " + StringUtils.join(attemptedUsernames, " / ") + "."; + + throw (new UsernameNotFoundException(errorMessage)); + } + + /** + * Attempt to find a CSpace user for a SAML response. + * + * @deprecated + * @param relyingPartyRegistration + * @param responseToken + * @return a CSpace user + */ + private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, ResponseToken responseToken) { + List assertions = responseToken.getResponse().getAssertions(); + return findUser(relyingPartyRegistration,assertions); + } } diff --git a/services/common/src/test/java/org/collectionspace/services/common/test/AbstractSecurityTestBase.java b/services/common/src/test/java/org/collectionspace/services/common/test/AbstractSecurityTestBase.java new file mode 100644 index 000000000..2fdd5da6e --- /dev/null +++ b/services/common/src/test/java/org/collectionspace/services/common/test/AbstractSecurityTestBase.java @@ -0,0 +1,209 @@ +package org.collectionspace.services.common.test; + +import java.io.ByteArrayInputStream; + +import javax.xml.bind.JAXBException; +import javax.xml.namespace.QName; + +import org.collectionspace.services.common.config.ServicesConfigReaderImpl; +import org.collectionspace.services.config.ServiceConfig; +import org.joda.time.DateTime; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.config.InitializationService; +import org.opensaml.core.xml.XMLObjectBuilder; +import org.opensaml.core.xml.XMLObjectBuilderFactory; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.XSString; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.BeforeSuite; + +public class AbstractSecurityTestBase { + private static final Logger logger = LoggerFactory.getLogger(SecurityUtilsTest.class); + protected static String BANNER = "-------------------------------------------------------"; + protected static String FRIENDLY_ATTR_NAME = "mail"; + protected static String ATTR_NAME = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; + protected static String ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; + protected static String EMAIL_ADDRESS = "example@example.org"; + protected static final String USERNAME_ATTRIBUTE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; + protected static final String SSOID_ATTRIBUTE = "http://schemas.auth0.com/identifier"; + protected static final String SSO_CONFIG_STRING = createDefaultTestConfig(); + + protected static String createDefaultTestConfig() { + return createTestConfig(USERNAME_ATTRIBUTE, SSOID_ATTRIBUTE); + } + + protected static String createTestConfig(String usernameAttribute, String ssoAttribute) { + return new StringBuilder().append("\n") + .append("") + .append("").append("").append("").append("") + .append("").append("") + .append("Auth0 - Scenario 11") + .append("") + .append("") + .append("").append("") + .append("").append("") + .append("").append("") + .append("").append("").append("") + .append("\n").append("").append("").toString(); + } + + protected static final String MOCK_ROOT_DIR = "./"; + + protected ServiceConfig parseServiceConfigString() throws JAXBException { + return parseServiceConfigString(MOCK_ROOT_DIR, SSO_CONFIG_STRING); + } + + protected ServiceConfig parseServiceConfigString(String mockRootDir, String seviceConfigString) + throws JAXBException { + ServicesConfigReaderImpl rdr = new ServicesConfigReaderImpl(mockRootDir); + ByteArrayInputStream in = new ByteArrayInputStream(seviceConfigString.getBytes()); + try { + serviceConfig = (ServiceConfig) rdr.parse(in, ServiceConfig.class); + } catch (JAXBException e) { + logger.warn("Could not create test service config: " + e.getLocalizedMessage()); + throw e; + } + return serviceConfig; + } + + /* for mocking useful SAML objects */ + protected T createNewSAMLObject(Class clazz) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); + + @SuppressWarnings("unchecked") // NOTE: the T extends SAMLObject ought to guarantee this works + T theObject = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); + return theObject; + } + + protected XSString createNewXSString(String value) { + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + @SuppressWarnings("unchecked") + XMLObjectBuilder stringBuilder = (XMLObjectBuilder) builderFactory + .getBuilder(XSString.TYPE_NAME); + XSString theString = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + theString.setValue(value); + return theString; + } + + // NOTE: making the assumption that OpenSAML parses an untyped attribute value + // into XSAny with value in the text content + protected XSAny createNewXSAny(String value) { + XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); + @SuppressWarnings("unchecked") + XMLObjectBuilder stringBuilder = (XMLObjectBuilder) builderFactory.getBuilder(XSAny.TYPE_NAME); + XSAny theAny = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSAny.TYPE_NAME); + theAny.setTextContent(value); + return theAny; + } + + protected Assertion createTestAssertionNoAttributes() + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + Assertion testAssertion = createNewSAMLObject(Assertion.class); + testAssertion.setVersion(SAMLVersion.VERSION_20); + testAssertion.setIssueInstant(new DateTime()); + + Subject testSubject = createNewSAMLObject(Subject.class); + NameID testNameId = createNewSAMLObject(NameID.class); + testNameId.setValue("test subject nameid"); + testSubject.setNameID(testNameId); + testAssertion.setSubject(testSubject); + + return testAssertion; + } + + protected Attribute createTestAttribute(boolean hasTypedAttributeValues, String attributeName, + String attributeNameFormat) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + Attribute attr = createNewSAMLObject(Attribute.class); + attr.setFriendlyName(FRIENDLY_ATTR_NAME); + attr.setName(attributeName); + attr.setNameFormat(attributeNameFormat); + if (hasTypedAttributeValues) { + XSString attrValue = createNewXSString(EMAIL_ADDRESS); + attr.getAttributeValues().add(attrValue); + } else { + XSAny attrValue = createNewXSAny(EMAIL_ADDRESS); + attr.getAttributeValues().add(attrValue); + } + + return attr; + } + + protected Attribute createDefaultTestAttribute(boolean hasTypedAttributeValues) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + return createTestAttribute(hasTypedAttributeValues, ATTR_NAME, ATTR_NAME_FORMAT); + } + + protected Assertion createTestAssertion(Attribute attribute) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + Assertion testAssertion = createTestAssertionNoAttributes(); + + AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class); + attrStmt.getAttributes().add(attribute); + testAssertion.getAttributeStatements().add(attrStmt); + + return testAssertion; + } + + protected Assertion createTestAssertionTypedAttributeValues() + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + return createTestAssertion(createDefaultTestAttribute(true)); + } + + protected Assertion createTestAssertionUntypedAttributeValues() + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + return createTestAssertion(createDefaultTestAttribute(false)); + } + + /* test suite setup below */ + protected Assertion testAssertionTypedAttributeValues = null; + protected Assertion testAssertionUntypedAttributeValues = null; + protected ServiceConfig serviceConfig = null; + + @BeforeSuite + protected void setup() throws InitializationException, NoSuchFieldException, IllegalAccessException, JAXBException { + /* try to set up openSAML */ + XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + try { + InitializationService.initialize(); + } catch (InitializationException e) { + logger.error("Could not initialize openSAML: " + e.getLocalizedMessage(), e); + throw e; + } + // try to create a test assertion with typed attribute values; fail the test if + // this doesn't work + try { + testAssertionTypedAttributeValues = createTestAssertionTypedAttributeValues(); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + logger.error("Could not create test assertion with typed attribute values: " + e.getLocalizedMessage(), e); + throw e; + } + // try to create a test assertion with untyped attribute values; fail the test + // if this doesn't work + try { + testAssertionUntypedAttributeValues = createTestAssertionUntypedAttributeValues(); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + logger.error("Could not create test assertion with untyped attribute values: " + e.getLocalizedMessage(), + e); + throw e; + } + + /* try to set up mock config */ + serviceConfig = parseServiceConfigString(); + } +} diff --git a/services/common/src/test/java/org/collectionspace/services/common/test/SecurityUtilsTest.java b/services/common/src/test/java/org/collectionspace/services/common/test/SecurityUtilsTest.java index 1677792fa..22b6fc62b 100644 --- a/services/common/src/test/java/org/collectionspace/services/common/test/SecurityUtilsTest.java +++ b/services/common/src/test/java/org/collectionspace/services/common/test/SecurityUtilsTest.java @@ -1,222 +1,127 @@ package org.collectionspace.services.common.test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.Assert; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.Test; -import org.w3c.dom.Element; - -import java.util.ArrayList; import java.util.Set; -import javax.xml.namespace.QName; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; +import javax.xml.bind.JAXBException; import org.collectionspace.services.common.security.SecurityUtils; -import org.collectionspace.services.config.AssertionAttributeProbeType; import org.collectionspace.services.config.AssertionProbesType; -import org.joda.time.DateTime; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.config.InitializationException; -import org.opensaml.core.config.InitializationService; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.XMLObjectBuilder; -import org.opensaml.core.xml.XMLObjectBuilderFactory; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.core.xml.schema.XSAny; -import org.opensaml.core.xml.schema.XSString; -import org.opensaml.saml.common.SAMLObject; -import org.opensaml.saml.common.SAMLVersion; -import org.opensaml.saml.saml2.core.AttributeStatement; -import org.opensaml.saml.saml2.core.AttributeValue; -import org.opensaml.saml.saml2.core.NameID; -import org.opensaml.saml.saml2.core.Subject; +import org.collectionspace.services.config.SAMLRelyingPartyRegistrationsType; +import org.collectionspace.services.config.SAMLRelyingPartyType; +import org.collectionspace.services.config.ServiceConfig; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; -public class SecurityUtilsTest { +public class SecurityUtilsTest extends AbstractSecurityTestBase { private static final Logger logger = LoggerFactory.getLogger(SecurityUtilsTest.class); - private static String BANNER = "-------------------------------------------------------"; - private static String FRIENDLY_ATTR_NAME = "mail"; - private static String ATTR_NAME = "urn:oid:0.9.2342.19200300.100.1.3"; - private static String ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; - private static String EMAIL_ADDRESS = "example@example.org"; - private void testBanner(String msg) { - logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER); - } - /* - private String xml2String(XMLObject xmlObject) { - Element element = null; - String xmlString = ""; - try { - Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(xmlObject); - out.marshall(xmlObject); - element = xmlObject.getDOM(); - - } catch (MarshallingException e) { - logger.error(e.getMessage(), e); - e.printStackTrace(); - } - - try { - Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - StreamResult result = new StreamResult(new java.io.StringWriter()); - - transformer.transform(new DOMSource(element), result); - xmlString = result.getWriter().toString(); - } catch (TransformerConfigurationException e) { - logger.error("Transformer configuration exception: " + e.getLocalizedMessage()); - e.printStackTrace(); - } catch (TransformerException e) { - logger.error("Exception in transformer: " + e.getLocalizedMessage()); - e.printStackTrace(); - } - - return xmlString; + + private void testBanner(String msg) { + logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER); } - */ - private T createNewSAMLObject(Class clazz) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); - QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); - - @SuppressWarnings("unchecked") // NOTE: the T extends SAMLObject ought to guarantee this works - T theObject = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); - return theObject; - } - private XSString createNewXSString(String value) { - XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); - @SuppressWarnings("unchecked") - XMLObjectBuilder stringBuilder = (XMLObjectBuilder) builderFactory.getBuilder(XSString.TYPE_NAME); - XSString theString = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); - theString.setValue(value); - return theString; - } - // NOTE: making the assumption that OpenSAML parses an untyped attribute value into XSAny with value in the text content - private XSAny createNewXSAny(String value) { - XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); - @SuppressWarnings("unchecked") - XMLObjectBuilder stringBuilder = (XMLObjectBuilder) builderFactory.getBuilder(XSAny.TYPE_NAME); - XSAny theAny = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME,XSAny.TYPE_NAME); - theAny.setTextContent(value); - return theAny; - } - private Assertion createTestAssertionNoAttributes() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - Assertion testAssertion = createNewSAMLObject(Assertion.class); - testAssertion.setVersion(SAMLVersion.VERSION_20); - testAssertion.setIssueInstant(new DateTime()); - - Subject testSubject = createNewSAMLObject(Subject.class); - NameID testNameId = createNewSAMLObject(NameID.class); - testNameId.setValue("test subject nameid"); - testSubject.setNameID(testNameId); - testAssertion.setSubject(testSubject); - - return testAssertion; - } - private Attribute createAttribute(boolean hasTypedAttributeValues) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - Attribute attr = createNewSAMLObject(Attribute.class); - attr.setFriendlyName(FRIENDLY_ATTR_NAME); - attr.setName(ATTR_NAME); - attr.setNameFormat(ATTR_NAME_FORMAT); - if(hasTypedAttributeValues) { - XSString attrValue = createNewXSString(EMAIL_ADDRESS); - attr.getAttributeValues().add(attrValue); - } - else { - XSAny attrValue = createNewXSAny(EMAIL_ADDRESS); - attr.getAttributeValues().add(attrValue); - } - - return attr; - } - private Assertion createTestAssertionTypedAttributeValues() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - Assertion testAssertion = createTestAssertionNoAttributes(); - - Attribute attr = createAttribute(true); - - AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class); - attrStmt.getAttributes().add(attr); - testAssertion.getAttributeStatements().add(attrStmt); - - return testAssertion; - } - private Assertion createTestAssertionUntypedAttributeValues() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - Assertion testAssertion = createTestAssertionNoAttributes(); - - Attribute attr = createAttribute(false); - - AttributeStatement attrStmt = createNewSAMLObject(AttributeStatement.class); - attrStmt.getAttributes().add(attr); - testAssertion.getAttributeStatements().add(attrStmt); - - return testAssertion; - } - - // the tests are below - private Assertion testAssertionTypedAttributeValues = null; - private Assertion testAssertionUntypedAttributeValues = null; - @BeforeSuite - private void setup() throws InitializationException,NoSuchFieldException,IllegalAccessException { - // try to set up openSAML - XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry(); - ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + + @Test + public void assertionWithTypedAttributeValuesIsNotNull() { + testBanner("the mock assertion with typed attribute values is not null"); + Assert.assertNotNull(testAssertionTypedAttributeValues); + } + + @Test + public void assertionWithUntypedAttributeValuesIsNotNull() { + testBanner("the mock assertion with untyped attribute values is not null"); + Assert.assertNotNull(testAssertionUntypedAttributeValues); + } + + @Test(dependsOnMethods = { "assertionWithTypedAttributeValuesIsNotNull" }) + public void candidateUsernamesTypedNotNullOrEmpty() { + testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string"); + Set candidateUsernames = SecurityUtils + .findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null); + Assert.assertNotNull(candidateUsernames); + if (null != candidateUsernames) + Assert.assertFalse(candidateUsernames.isEmpty()); + } + + @Test(dependsOnMethods = { "assertionWithUntypedAttributeValuesIsNotNull" }) + public void candidateUsernamesUntypedNotNullOrEmpty() { + testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed"); + Set candidateUsernames = SecurityUtils + .findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null); + Assert.assertNotNull(candidateUsernames); + if (null != candidateUsernames) + Assert.assertFalse(candidateUsernames.isEmpty()); + } + + @Test(dependsOnMethods = { "assertionWithUntypedAttributeValuesIsNotNull" }) + public void candidateUsernamesUntypedIsCorrect() { + testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed"); + Set candidateUsernames = SecurityUtils + .findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null); + Assert.assertNotNull(candidateUsernames); + if (null != candidateUsernames) + Assert.assertEquals(candidateUsernames.iterator().next(), EMAIL_ADDRESS); + } + + @Test(dependsOnMethods = { "assertionWithTypedAttributeValuesIsNotNull" }) + public void candidateUsernamesTypedIsCorrect() { + testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string"); + Set candidateUsernames = SecurityUtils + .findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null); + Assert.assertNotNull(candidateUsernames); + if (null != candidateUsernames) + Assert.assertEquals(candidateUsernames.iterator().next(), EMAIL_ADDRESS); + } + + @Test + public void idenfitiferProbeFindsSsoId() throws JAXBException, IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, SecurityException { + testBanner("identifier probe finds sso id"); + + String nameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; + String identifierName = "http://schemas.auth0.com/identifier"; + + // set up a minimal mock configuration string with the SSO ID probe we wish to + // test + String theConfigString = createTestConfig(USERNAME_ATTRIBUTE, identifierName); + ServiceConfig theServiceConfig = null; try { - InitializationService.initialize(); - } catch (InitializationException e) { - logger.error("Could not initialize openSAML: " + e.getLocalizedMessage(), e); + theServiceConfig = parseServiceConfigString(MOCK_ROOT_DIR, theConfigString); + } catch (JAXBException e) { + logger.warn("Could not create mock service config: " + e.getLocalizedMessage()); throw e; - } - // try to create a test assertion with typed attribute values; fail the test if this doesn't work + } + SAMLRelyingPartyRegistrationsType relyingPartyRegistrations = theServiceConfig.getSecurity().getSso().getSaml() + .getRelyingPartyRegistrations(); + SAMLRelyingPartyType relyingPartyRegistration = relyingPartyRegistrations.getRelyingParty().get(0); + AssertionProbesType assertionSsoIdProbes = (relyingPartyRegistration != null + ? relyingPartyRegistration.getAssertionSsoIdProbes() + : null); + + // create an attribute with the same name identifier as the test probe + Attribute attribute = null; try { - testAssertionTypedAttributeValues = createTestAssertionTypedAttributeValues(); + attribute = createTestAttribute(true, identifierName, nameFormat); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { - logger.error("Could not create test assertion with typed attribute values: " + e.getLocalizedMessage(), e); + logger.warn("Could not create mock attribute: " + e.getLocalizedMessage()); throw e; } - // try to create a test assertion with untyped attribute values; fail the test if this doesn't work + // create a SAML assertion with the attribute + Assertion assertion = null; try { - testAssertionUntypedAttributeValues = createTestAssertionUntypedAttributeValues(); + assertion = createTestAssertion(attribute); } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { - logger.error("Could not create test assertion with untyped attribute values: " + e.getLocalizedMessage(), e); + logger.warn("Could not create SAML assertion" + e.getLocalizedMessage()); throw e; } - } - - @Test - public void assertionWithTypedAttributeValuesIsNotNull() { - testBanner("the mock assertion with typed attribute values is not null"); - Assert.assertNotNull(testAssertionTypedAttributeValues); - } - @Test - public void assertionWithUntypedAttributeValuesIsNotNull() { - testBanner("the mock assertion with untyped attribute values is not null"); - Assert.assertNotNull(testAssertionUntypedAttributeValues); - } - @Test(dependsOnMethods = {"assertionWithTypedAttributeValuesIsNotNull"}) - public void candidateUsernamesTypedNotNullOrEmpty() { - testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are typed as string"); - Set candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(testAssertionTypedAttributeValues, null); - Assert.assertNotNull(candidateUsernames); - if(null != candidateUsernames) - Assert.assertFalse(candidateUsernames.isEmpty()); - } - @Test(dependsOnMethods = {"assertionWithUntypedAttributeValuesIsNotNull"}) - public void candidateUsernamesUntypedNotNullOrEmpty() { - testBanner("findSamlAssertionCandidateUsernames finds candidate usernames when they are not typed"); - Set candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(testAssertionUntypedAttributeValues, null); - Assert.assertNotNull(candidateUsernames); - if(null != candidateUsernames) - Assert.assertFalse(candidateUsernames.isEmpty()); - } + + // check whether getSamlAssertionSsoId finds the SSO ID we put in the assertion + // using the test probe + String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes); + Assert.assertNotNull(ssoId); + Assert.assertFalse(ssoId.isEmpty()); + Assert.assertEquals(ssoId, EMAIL_ADDRESS); + } } diff --git a/services/config/pom.xml b/services/config/pom.xml index 1adf618ee..71136d895 100644 --- a/services/config/pom.xml +++ b/services/config/pom.xml @@ -30,6 +30,17 @@ javax.xml.bind jaxb-api + + com.sun.xml.bind + jaxb-core + test + + + xerces + xercesImpl + 2.12.2 + test + org.jvnet.jaxb2_commons jaxb2-basics diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java b/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java index eff40cbf4..a03e118f3 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/AbstractConfigReaderImpl.java @@ -121,7 +121,7 @@ public abstract class AbstractConfigReaderImpl implements ConfigReader { return getFileChildren(rootDir, true); } - protected Object parse(File configFile, Class clazz) + public Object parse(File configFile, Class clazz) throws FileNotFoundException, JAXBException { Object result = null; @@ -144,7 +144,7 @@ public abstract class AbstractConfigReaderImpl implements ConfigReader { * @throws JAXBException * @throws Exception */ - protected Object parse(InputStream configFileStream, Class clazz) + public Object parse(InputStream configFileStream, Class clazz) throws JAXBException { Object result = null; diff --git a/services/config/src/test/java/org/collectionspace/services/common/config/ServicesConfigReaderImplTest.java b/services/config/src/test/java/org/collectionspace/services/common/config/ServicesConfigReaderImplTest.java new file mode 100644 index 000000000..73c612ccf --- /dev/null +++ b/services/config/src/test/java/org/collectionspace/services/common/config/ServicesConfigReaderImplTest.java @@ -0,0 +1,139 @@ +package org.collectionspace.services.common.config; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.collectionspace.services.config.AssertionAttributeProbeType; +import org.collectionspace.services.config.SAMLRelyingPartyType; +import org.collectionspace.services.config.SAMLType; +import org.collectionspace.services.config.ServiceConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Test; + +public class ServicesConfigReaderImplTest { + private static final Logger logger = LoggerFactory.getLogger(ServicesConfigReaderImplTest.class); + private static String BANNER = "-------------------------------------------------------"; + // NOTE: adapted from https://collectionspace.atlassian.net/browse/DRYD-1702?focusedCommentId=60649 + private static final String USERNAME_ATTRIBUTE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; + private static final String SSOID_ATTRIBUTE = "http://schemas.auth0.com/identifier"; + private static final String SSO_CONFIG_STRING = new StringBuilder() + .append("\n") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("Auth0 - Scenario 11") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("\n") + .append("") + .append("") + .toString(); + private static final String MOCK_ROOT_DIR = "./"; + private void testBanner(String msg) { + logger.info("\r" + BANNER + "\r\n" + this.getClass().getName() + "\r\n" + msg + "\r\n" + BANNER); + } + private List getUserNameProbesFromConfig() { + return getUserNameProbesFromConfig(serviceConfig); + } + private List getUserNameProbesFromConfig(ServiceConfig serviceConfig) { + SAMLType samlConfig = serviceConfig.getSecurity().getSso().getSaml(); + List relyingParties = samlConfig.getRelyingPartyRegistrations().getRelyingParty(); + SAMLRelyingPartyType relyingParty = relyingParties.get(0); + + List usernameProbes = relyingParty.getAssertionUsernameProbes().getNameIdOrAttribute(); + ArrayList up = new ArrayList(); + for (Object obj : usernameProbes) { + AssertionAttributeProbeType a = (AssertionAttributeProbeType) obj; + up.add(a); + } + + return up; + } + private List getSsoIdProbesFromConfig() { + return getSsoIdProbesFromConfig(serviceConfig); + } + private List getSsoIdProbesFromConfig(ServiceConfig serviceConfig) { + SAMLType samlConfig = serviceConfig.getSecurity().getSso().getSaml(); + List relyingParties = samlConfig.getRelyingPartyRegistrations().getRelyingParty(); + SAMLRelyingPartyType relyingParty = relyingParties.get(0); + + List ssoIdProbes = relyingParty.getAssertionSsoIdProbes().getNameIdOrAttribute(); + ArrayList up = new ArrayList(); + for (Object obj : ssoIdProbes) { + AssertionAttributeProbeType a = (AssertionAttributeProbeType) obj; + up.add(a); + } + + return up; + } + // the tests are below + private ServiceConfig serviceConfig = null; + @BeforeSuite + public void setup() throws JAXBException { + ServicesConfigReaderImpl rdr = new ServicesConfigReaderImpl(MOCK_ROOT_DIR); + ByteArrayInputStream in = new ByteArrayInputStream(SSO_CONFIG_STRING.getBytes()); + try { + serviceConfig = (ServiceConfig) rdr.parse(in, ServiceConfig.class); + } catch (JAXBException e) { + logger.warn("Could not create test service config: " + e.getLocalizedMessage()); + throw e; + } + } + @Test + public void usernameProbesNotNullOrEmpty() { + testBanner("the username probes list is not null or empty"); + + List usernameProbes = getUserNameProbesFromConfig(); + Assert.assertNotNull(usernameProbes); + if(null != usernameProbes) { + Assert.assertFalse(usernameProbes.isEmpty()); + } + } + @Test(dependsOnMethods = {"usernameProbesNotNullOrEmpty"}) + public void usernameProbesCorrectlyParsedFromConfig() { + testBanner("the username probes list has expected contents"); + + List usernameProbes = getUserNameProbesFromConfig(); + Assert.assertEquals(usernameProbes.size(), 1); + AssertionAttributeProbeType probe = usernameProbes.get(0); + Assert.assertEquals(probe.getName(), USERNAME_ATTRIBUTE); + } + @Test + public void ssoIdProbesNotNullOrEmpty() { + testBanner("the SSO ID probes list is not null or empty"); + + List ssoIdProbes = getSsoIdProbesFromConfig(); + Assert.assertNotNull(ssoIdProbes); + if(null != ssoIdProbes) { + Assert.assertFalse(ssoIdProbes.isEmpty()); + } + } + @Test(dependsOnMethods = {"ssoIdProbesNotNullOrEmpty"}) + public void ssoIdProbesCorrectlyParsedFromConfig() { + testBanner("the SSO ID probes list has expected contents"); + + List ssoIdProbes = getSsoIdProbesFromConfig(); + Assert.assertEquals(ssoIdProbes.size(), 1); + AssertionAttributeProbeType probe = ssoIdProbes.get(0); + Assert.assertEquals(probe.getName(), SSOID_ATTRIBUTE); + } +} -- 2.47.3