/** * ============================================================================= * * ORCID (R) Open Source * http://orcid.org * * Copyright (c) 2012-2014 ORCID, Inc. * Licensed under an MIT-Style License (MIT) * http://orcid.org/open-source-license * * This copyright and license information (including a link to the full license) * shall be included in its entirety in all copies or substantial portion of * the software. * * ============================================================================= */ package org.orcid.core.manager.impl; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Resource; import javax.ws.rs.core.MediaType; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.lang3.StringUtils; import org.orcid.core.locale.LocaleManager; import org.orcid.core.manager.IdentityProviderManager; import org.orcid.core.utils.NamespaceMap; import org.orcid.persistence.dao.IdentityProviderDao; import org.orcid.persistence.jpa.entities.IdentityProviderEntity; import org.orcid.persistence.jpa.entities.IdentityProviderNameEntity; import org.orcid.utils.ReleaseNameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; /** * * @author Will Simpson * */ public class IdentityProviderManagerImpl implements IdentityProviderManager { private static final Logger LOGGER = LoggerFactory.getLogger(IdentityProviderManagerImpl.class); @Value("${org.orcid.core.idpMetadataUrlsSpaceSeparated:http://www.testshib.org/metadata/testshib-providers.xml https://engine.surfconext.nl/authentication/idp/metadata}") private String metadataUrlsString; @Resource private IdentityProviderDao identityProviderDao; @Resource private TransactionTemplate transactionTemplate; @Resource private LocaleManager localeManager; @Resource(name = "identityProviderNameCache") private SelfPopulatingCache identityProviderNameCache; private String releaseName = ReleaseNameUtils.getReleaseName(); private Pattern mailtoPattern = Pattern.compile("^mailto:"); @Override public String retrieveIdentitifyProviderName(String providerid) { return retrieveIdentitifyProviderName(providerid, localeManager.getLocale()); } @Override public String retrieveIdentitifyProviderName(String providerid, Locale locale) { return (String) identityProviderNameCache.get(new IdentityProviderNameCacheKey(providerid, locale, releaseName)).getObjectValue(); } @Override public String retrieveFreshIdentitifyProviderName(String providerid, Locale locale) { IdentityProviderEntity idp = identityProviderDao.findByProviderid(providerid); List<IdentityProviderNameEntity> names = idp.getNames(); if (names != null) { Optional<IdentityProviderNameEntity> idpNameEntity = names.stream().filter(n -> n.getLang().equals(locale.getLanguage())).findFirst(); if (idpNameEntity.isPresent()) { return idpNameEntity.get().getDisplayName(); } } return idp.getDisplayName(); } @Override public String retrieveContactEmailByProviderid(String providerid) { IdentityProviderEntity idp = identityProviderDao.findByProviderid(providerid); if (idp == null) { return null; } String supportEmail = idp.getSupportEmail(); if (supportEmail != null) { return supportEmail; } List<String> otherEmails = new ArrayList<>(2); otherEmails.add(idp.getAdminEmail()); otherEmails.add(idp.getTechEmail()); return String.join(";", otherEmails.stream().filter(e -> e != null).collect(Collectors.toList())); } @Override public void loadIdentityProviders() { String[] metadataUrls = StringUtils.split(metadataUrlsString); XPath xpath = createXPath(); XPathExpression entityDescriptorXpath = compileXPath(xpath, "//md:EntityDescriptor"); for (String metadataUrl : metadataUrls) { Document document = downloadMetadata(metadataUrl); NodeList nodes = evaluateXPathNodeList(entityDescriptorXpath, document); for (int i = 0; i < nodes.getLength(); i++) { Element element = (Element) nodes.item(i); IdentityProviderEntity incoming = createEntityFromXml(element); LOGGER.info("Found identity provider: {}", incoming.toShortString()); saveOrUpdateIdentityProvider(incoming); } } } @Override public IdentityProviderEntity createEntityFromXml(Element idpElement) { XPath xpath = createXPath(); XPathExpression mainDisplayNameXpath = compileXPath(xpath, "string(.//md:IDPSSODescriptor//mdui:DisplayName[1])"); XPathExpression displayNamesXpath = compileXPath(xpath, ".//md:IDPSSODescriptor//mdui:DisplayName"); XPathExpression legacyMainDisplayNameXpath = compileXPath(xpath, "string(.//md:OrganizationDisplayName[1])"); XPathExpression legacyDisplayNamesXpath = compileXPath(xpath, ".//md:OrganizationDisplayName"); XPathExpression supportContactXpath = compileXPath(xpath, "string((.//md:ContactPerson[@contactType='support'])[1]/md:EmailAddress[1])"); XPathExpression adminContactXpath = compileXPath(xpath, "string((.//md:ContactPerson[@contactType='administrative'])[1]/md:EmailAddress[1])"); XPathExpression techContactXpath = compileXPath(xpath, "string((.//md:ContactPerson[@contactType='technical'])[1]/md:EmailAddress[1])"); String entityId = idpElement.getAttribute("entityID"); String mainDisplayName = evaluateXPathString(mainDisplayNameXpath, idpElement); if (StringUtils.isBlank(mainDisplayName)) { mainDisplayName = evaluateXPathString(legacyMainDisplayNameXpath, idpElement); } String supportEmail = tidyEmail(evaluateXPathString(supportContactXpath, idpElement)); String adminEmail = tidyEmail(evaluateXPathString(adminContactXpath, idpElement)); String techEmail = tidyEmail(evaluateXPathString(techContactXpath, idpElement)); List<IdentityProviderNameEntity> nameEntities = createNameEntitiesFromXml(displayNamesXpath, idpElement); if (nameEntities == null || nameEntities.isEmpty()) { nameEntities = createNameEntitiesFromXml(legacyDisplayNamesXpath, idpElement); } IdentityProviderEntity identityProviderEntity = new IdentityProviderEntity(); identityProviderEntity.setProviderid(entityId); identityProviderEntity.setDisplayName(mainDisplayName); identityProviderEntity.setNames(nameEntities); identityProviderEntity.setSupportEmail(supportEmail); identityProviderEntity.setAdminEmail(adminEmail); identityProviderEntity.setTechEmail(techEmail); return identityProviderEntity; } private List<IdentityProviderNameEntity> createNameEntitiesFromXml(XPathExpression displayNamesXpath, Element idpElement) { List<IdentityProviderNameEntity> nameEntities = new ArrayList<>(); NodeList displayNames = evaluateXPathNodeList(displayNamesXpath, idpElement); if (displayNames != null) { for (int i = 0; i < displayNames.getLength(); i++) { Element displayNameElement = (Element) displayNames.item(i); String lang = displayNameElement.getAttribute("xml:lang"); String displayName = displayNameElement.getTextContent(); IdentityProviderNameEntity nameEntity = new IdentityProviderNameEntity(); nameEntity.setLang(lang); nameEntity.setDisplayName(displayName); nameEntities.add(nameEntity); } } return nameEntities; } private Document downloadMetadata(String metadataUrl) { LOGGER.info("About to download idp metadata from {}", metadataUrl); Client client = Client.create(); WebResource resource = client.resource(metadataUrl); ClientResponse response = resource.accept(MediaType.APPLICATION_XML).get(ClientResponse.class); Document document = response.getEntity(Document.class); return document; } private void saveOrUpdateIdentityProvider(IdentityProviderEntity incoming) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { saveOrUpdateIdentityProviderInTransaction(incoming); } }); } private void saveOrUpdateIdentityProviderInTransaction(IdentityProviderEntity incoming) { IdentityProviderEntity existing = identityProviderDao.findByProviderid(incoming.getProviderid()); if (existing == null) { incoming.getNames().stream().forEach(i -> i.setIdentityProvider(incoming)); identityProviderDao.persist(incoming); } else { existing.setProviderid(incoming.getProviderid()); existing.setDisplayName(incoming.getDisplayName()); existing.setSupportEmail(incoming.getSupportEmail()); existing.setAdminEmail(incoming.getAdminEmail()); existing.setTechEmail(incoming.getTechEmail()); mergeNames(incoming, existing); identityProviderDao.merge(existing); } } private void mergeNames(IdentityProviderEntity incoming, IdentityProviderEntity existing) { List<IdentityProviderNameEntity> existingNames = existing.getNames(); List<IdentityProviderNameEntity> incomingNames = incoming.getNames(); if (existingNames != null) { Map<String, IdentityProviderNameEntity> incomingNamesMappedByLang = incomingNames.stream() .collect(Collectors.toMap(IdentityProviderNameEntity::getLang, Function.identity())); // Update existing name entities existingNames.stream().forEach(e -> { IdentityProviderNameEntity incomingName = incomingNamesMappedByLang.get(e.getLang()); if (incomingName != null) { e.setDisplayName(incoming.getDisplayName()); } }); // Remove existing names that are not in the incoming // list existingNames.removeIf(e -> { return incomingNames.stream().noneMatch(i -> { String existingDisplayName = e.getDisplayName(); return i.getDisplayName().equals(existingDisplayName); }); }); // Add new names Map<String, IdentityProviderNameEntity> existingNamesMappedByLang = existingNames.stream() .collect(Collectors.toMap(IdentityProviderNameEntity::getLang, Function.identity())); incomingNames.stream().forEach(i -> { if (!existingNamesMappedByLang.containsKey(i.getLang())) { i.setIdentityProvider(existing); existingNames.add(i); } }); } else { incomingNames.stream().forEach(i -> i.setIdentityProvider(incoming)); existing.setNames(incomingNames); } } private NodeList evaluateXPathNodeList(XPathExpression xpathExpression, Node node) { try { return (NodeList) xpathExpression.evaluate(node, XPathConstants.NODESET); } catch (XPathExpressionException e) { throw new RuntimeException("Problem evaluating xpath node list", e); } } private String evaluateXPathString(XPathExpression xpathExpression, Node node) { try { return (String) xpathExpression.evaluate(node, XPathConstants.STRING); } catch (XPathExpressionException e) { throw new RuntimeException("Problem evaluating xpath string", e); } } private XPathExpression compileXPath(XPath xpath, String expression) { try { return xpath.compile(expression); } catch (XPathExpressionException e) { throw new RuntimeException("Problem compiling xpath: " + expression, e); } } private XPath createXPath() { XPath xpath = XPathFactory.newInstance().newXPath(); NamespaceMap namespaceMap = new NamespaceMap(); namespaceMap.putNamespace("md", "urn:oasis:names:tc:SAML:2.0:metadata"); namespaceMap.putNamespace("mdui", "urn:oasis:names:tc:SAML:metadata:ui"); xpath.setNamespaceContext(namespaceMap); return xpath; } private String tidyEmail(String email) { if (StringUtils.isBlank(email)) { return null; } return mailtoPattern.matcher(email).replaceAll("").trim(); } @Override public void incrementFailedCount(String shibIdentityProvider) { identityProviderDao.incrementFailedCount(shibIdentityProvider); } }