/** * ============================================================================= * * 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.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.Resource; import org.apache.commons.lang3.StringUtils; import org.orcid.core.manager.ClientDetailsEntityCacheManager; import org.orcid.core.manager.InstitutionalSignInManager; import org.orcid.core.manager.NotificationManager; import org.orcid.core.manager.SlackManager; import org.orcid.core.oauth.OrcidOauth2TokenDetailService; import org.orcid.core.utils.JsonUtils; import org.orcid.persistence.dao.UserConnectionDao; import org.orcid.persistence.jpa.entities.ClientDetailsEntity; import org.orcid.persistence.jpa.entities.UserConnectionStatus; import org.orcid.persistence.jpa.entities.UserconnectionEntity; import org.orcid.persistence.jpa.entities.UserconnectionPK; import org.orcid.pojo.HeaderCheckResult; import org.orcid.pojo.HeaderMismatch; import org.orcid.pojo.RemoteUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Transactional; public class InstitutionalSignInManagerImpl implements InstitutionalSignInManager { private static final Logger LOGGER = LoggerFactory.getLogger(InstitutionalSignInManagerImpl.class); private static final String SEPARATOR = ";"; private static final Pattern ATTRIBUTE_SEPARATOR_PATTERN = Pattern.compile("(?<!\\\\)" + SEPARATOR); private static final Pattern ESCAPED_SEPARATOR_PATTERN = Pattern.compile("\\\\" + SEPARATOR); @Resource protected UserConnectionDao userConnectionDao; @Resource protected OrcidUrlManager orcidUrlManager; @Resource protected ClientDetailsEntityCacheManager clientDetailsEntityCacheManager; @Resource protected OrcidOauth2TokenDetailService orcidOauth2TokenDetailService; @Resource protected NotificationManager notificationManager; @Resource private SlackManager slackManager; @Override @Transactional public void createUserConnectionAndNotify(String idType, String remoteUserId, String displayName, String providerId, String userOrcid, Map<String, String> headers) throws UnsupportedEncodingException { UserconnectionEntity userConnectionEntity = userConnectionDao.findByProviderIdAndProviderUserIdAndIdType(remoteUserId, providerId, idType); if (userConnectionEntity == null) { LOGGER.info("No user connection found for idType={}, remoteUserId={}, displayName={}, providerId={}, userOrcid={}", new Object[] { idType, remoteUserId, displayName, providerId, userOrcid }); userConnectionEntity = new UserconnectionEntity(); String randomId = Long.toString(new Random(Calendar.getInstance().getTimeInMillis()).nextLong()); UserconnectionPK pk = new UserconnectionPK(randomId, providerId, remoteUserId); userConnectionEntity.setOrcid(userOrcid); userConnectionEntity.setProfileurl(orcidUrlManager.getBaseUriHttp() + "/" + userOrcid); userConnectionEntity.setDisplayname(displayName); userConnectionEntity.setRank(1); userConnectionEntity.setId(pk); userConnectionEntity.setLinked(true); userConnectionEntity.setLastLogin(new Date()); userConnectionEntity.setIdType(idType); userConnectionEntity.setConnectionSatus(UserConnectionStatus.NOTIFIED); userConnectionEntity.setHeadersJson(JsonUtils.convertToJsonString(headers)); userConnectionDao.persist(userConnectionEntity); } else { LOGGER.info("Found existing user connection, {}", userConnectionEntity); } sendNotification(userOrcid, providerId); } @Override public void sendNotification(String userOrcid, String providerId) throws UnsupportedEncodingException { try { ClientDetailsEntity clientDetails = clientDetailsEntityCacheManager.retrieveByIdP(providerId); boolean clientKnowsUser = orcidOauth2TokenDetailService.doesClientKnowUser(clientDetails.getClientId(), userOrcid); // If the client doesn't know about the user yet, send a // notification if (!clientKnowsUser) { notificationManager.sendAcknowledgeMessage(userOrcid, clientDetails.getClientId()); } } catch (IllegalArgumentException e) { // The provided IdP hasn't not been linked to any client yet. } } @Override public HeaderCheckResult checkHeaders(Map<String, String> originalHeaders, Map<String, String> currentHeaders) { HeaderCheckResult result = new HeaderCheckResult(); List<String> headersToCheck = new ArrayList<>(); headersToCheck.addAll(Arrays.asList(POSSIBLE_REMOTE_USER_HEADERS)); headersToCheck.add(EPPN_HEADER); for (String headerName : headersToCheck) { String original = originalHeaders.get(headerName); String current = currentHeaders.get(headerName); // Only compare where both are not blank, because otherwise could // just be an IdP config change to add/remove the attribute if (StringUtils.isNoneBlank(original, current)) { Set<String> originalDeduped = dedupe(original); Set<String> currentDeduped = dedupe(current); if (!currentDeduped.equals(originalDeduped)) { result.addMismatch(new HeaderMismatch(headerName, original, current)); } } } if (!result.isSuccess()) { String message = String.format("Institutional sign in header check failed: %s, originalHeaders=%s", result, originalHeaders); LOGGER.info(message); slackManager.sendSystemAlert(message); } return result; } private Set<String> dedupe(String headerValue) { String[] values = ATTRIBUTE_SEPARATOR_PATTERN.split(headerValue); Set<String> deduped = new HashSet<>(); for (String value : values) { deduped.add(value); } return deduped; } @Override public RemoteUser retrieveRemoteUser(Map<String, String> headers) { for (String possibleHeader : InstitutionalSignInManager.POSSIBLE_REMOTE_USER_HEADERS) { String userId = extractFirst(headers.get(possibleHeader)); if (userId != null) { return new RemoteUser(userId, possibleHeader); } } return null; } @Override public String retrieveDisplayName(Map<String, String> headers) { String eppn = extractFirst(headers.get(InstitutionalSignInManager.EPPN_HEADER)); if (StringUtils.isNotBlank(eppn)) { return eppn; } String displayName = extractFirst(headers.get(InstitutionalSignInManager.DISPLAY_NAME_HEADER)); if (StringUtils.isNotBlank(displayName)) { return displayName; } String givenName = extractFirst(headers.get(InstitutionalSignInManager.GIVEN_NAME_HEADER)); String sn = extractFirst(headers.get(InstitutionalSignInManager.SN_HEADER)); String combinedNames = StringUtils.join(new String[] { givenName, sn }, ' '); if (StringUtils.isNotBlank(combinedNames)) { return combinedNames; } RemoteUser remoteUser = retrieveRemoteUser(headers); if (remoteUser != null) { String remoteUserId = remoteUser.getUserId(); if (StringUtils.isNotBlank(remoteUserId)) { int indexOfBang = remoteUserId.lastIndexOf("!"); if (indexOfBang != -1) { return remoteUserId.substring(indexOfBang); } else { return remoteUserId; } } } return null; } /** * Shibboleth SP combines multiple values by concatenating, using semicolon * as the separator (the escape character is '\'). Mutliple values will be * provided, even if it is actually the same attribute in mace and oid * format. * * @param headerValue * @return the first attribute value */ private static String extractFirst(String headerValue) { if (headerValue == null) { return null; } String[] values = ATTRIBUTE_SEPARATOR_PATTERN.split(headerValue); return values.length > 0 ? ESCAPED_SEPARATOR_PATTERN.matcher(values[0]).replaceAll(SEPARATOR) : ""; } }