/** * ============================================================================= * * 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.security.visibility.filter.impl; import java.security.AccessControlException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import javax.annotation.Resource; import org.orcid.core.security.PermissionChecker; import org.orcid.core.security.visibility.filter.VisibilityFilter; import org.orcid.core.tree.TreeCleaner; import org.orcid.core.tree.TreeCleaningDecision; import org.orcid.core.tree.TreeCleaningStrategy; import org.orcid.jaxb.model.message.Address; import org.orcid.jaxb.model.message.Affiliation; import org.orcid.jaxb.model.message.Country; import org.orcid.jaxb.model.message.Email; import org.orcid.jaxb.model.message.Funding; import org.orcid.jaxb.model.message.Orcid; import org.orcid.jaxb.model.message.OrcidIdentifier; import org.orcid.jaxb.model.message.OrcidMessage; import org.orcid.jaxb.model.message.OrcidProfile; import org.orcid.jaxb.model.message.OrcidSearchResults; import org.orcid.jaxb.model.message.OrcidWork; import org.orcid.jaxb.model.message.PrivateVisibleToSource; import org.orcid.jaxb.model.message.ScopePathType; import org.orcid.jaxb.model.message.Source; import org.orcid.jaxb.model.message.Visibility; import org.orcid.jaxb.model.message.VisibilityType; import org.orcid.jaxb.model.message.WorkContributors; import org.orcid.pojo.ajaxForm.PojoUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; /** * I would imagine the first time you see this class, it may be a bit confusing. * <p/> * The goal of this class is to insure that no elements that are marked with a * {@link org.orcid.jaxb.model.message .Visibility} are displayed to the caller * unless they have sufficient permissions. * <p/> * In addition, we need to make sure that the actual visibility attributes are * removed (if requested with the removeAttribute argument) are nulled in order * for callers to be unaware of elements that have been requested to be made * unavailable. * <p/> * This is a very important step in the security model and thus, reflection was * chosen to scan the JaxB objects and locate any classes implementing the * {@link org.orcid.jaxb.model.message.VisibilityType} interface and check on * the visibility level requested. * <p/> * * @author Declan Newman (declan) Date: 16/03/2012 */ @Component("visibilityFilter") public class VisibilityFilterImpl implements VisibilityFilter { private final static Logger LOGGER = LoggerFactory.getLogger(VisibilityFilterImpl.class); @Resource private PermissionChecker permissionChecker; /** * Remove the elements that are not present in the list of set of * {@link org.orcid.jaxb.model.message .Visibility}s present in the array * passed in By default the remaining visibility elements will not be * removed from the object. * * @param messageToBeFiltered * the {@link org.orcid.jaxb.model.message.OrcidMessage} that * will be traversed looking for * {@link org .orcid.jaxb.model.message.VisibilityType} * @param visibilities * What {@link org.orcid.jaxb.model.message.Visibility} elements * should be allowed. * @return the cleansed {@link org.orcid.jaxb.model.message.OrcidMessage} */ @Override public OrcidMessage filter(OrcidMessage messageToBeFiltered, Visibility... visibilities) { return filter(messageToBeFiltered, null, false, false, false, visibilities); } /** * Remove the elements that are not present in the list of set of * {@link org.orcid.jaxb.model.message .Visibility}s present in the array * passed in. * * @param messageToBeFiltered * the {@link org.orcid.jaxb.model.message.OrcidMessage} that * will be traversed looking for * {@link org .orcid.jaxb.model.message.VisibilityType} elements. * @param source * The orcid source that is executing the request * @param removeAttribute * should all {@link org.orcid.jaxb.model.message.Visibility} * elements be removed from the object graph. This has the effect * that they will not be present in the resulting JAXB * serialisation. * @param visibilities * What {@link org.orcid.jaxb.model.message.Visibility} elements * should be allowed. * @return the cleansed {@link org.orcid.jaxb.model.message.OrcidMessage} */ @Override public OrcidMessage filter(OrcidMessage messageToBeFiltered, final String sourceId, final boolean allowPrivateWorks, final boolean allowPrivateFunding, final boolean allowPrivateAffiliations, Visibility... visibilities) { if (messageToBeFiltered == null || visibilities == null || visibilities.length == 0) { return null; } String messageIdForLog = getMessageIdForLog(messageToBeFiltered); LOGGER.debug("About to filter message: " + messageIdForLog); final Set<Visibility> visibilitySet = new HashSet<Visibility>(Arrays.asList(visibilities)); if (visibilitySet.contains(Visibility.SYSTEM)) { return messageToBeFiltered; } else { TreeCleaner treeCleaner = new TreeCleaner(); treeCleaner.clean(messageToBeFiltered, new TreeCleaningStrategy() { public TreeCleaningDecision needsStripping(Object obj) { TreeCleaningDecision decision = TreeCleaningDecision.DEFAULT; if (obj != null) { Class<?> clazz = obj.getClass(); if (!PojoUtil.isEmpty(sourceId)) { if (allowPrivateAffiliations && Affiliation.class.isAssignableFrom(clazz)) { Affiliation affiliation = (Affiliation) obj; Source source = affiliation.getSource(); if (source != null) { String sourcePath = source.retrieveSourcePath(); if (sourcePath != null) { if (sourceId.equals(sourcePath)) { decision = TreeCleaningDecision.IGNORE; } } } } else if (allowPrivateFunding && Funding.class.isAssignableFrom(clazz)) { Funding funding = (Funding) obj; Source source = funding.getSource(); if (source != null) { String sourcePath = source.retrieveSourcePath(); if (sourcePath != null) { if (sourceId.equals(sourcePath)) { decision = TreeCleaningDecision.IGNORE; } } } } else if (allowPrivateWorks && OrcidWork.class.isAssignableFrom(clazz)) { OrcidWork work = (OrcidWork) obj; Source source = work.getSource(); if (source != null) { if (sourceId.equals(source.retrieveSourcePath())) { decision = TreeCleaningDecision.IGNORE; } } } } // If it is the address field, the visibility and source // fields are inside the country element if (Address.class.isAssignableFrom(clazz)) { Address address = (Address) obj; // Remove empty addresses if (address.getCountry() == null) { decision = TreeCleaningDecision.CLEANING_REQUIRED; } else { Country country = address.getCountry(); // Allow public addresses if (Visibility.PUBLIC.equals(country.getVisibility())) { decision = TreeCleaningDecision.IGNORE; } else if (visibilitySet.contains(Visibility.LIMITED)) { // Allow limited visibility when possible if (Visibility.LIMITED.equals(country.getVisibility())) { decision = TreeCleaningDecision.IGNORE; } else { // As last resource, check the source Source source = country.getSource(); if (source != null && sourceId != null && sourceId.equals(source.retrieveSourcePath())) { decision = TreeCleaningDecision.IGNORE; } else { decision = TreeCleaningDecision.CLEANING_REQUIRED; } } } } } if (Email.class.isAssignableFrom(clazz)) { // check for /email/read-private permissions, // include all emails if present try { Authentication authentication = getAuthentication(); if (authentication != null && messageToBeFiltered.getOrcidProfile() != null) { permissionChecker.checkPermissions(getAuthentication(), ScopePathType.EMAIL_READ_PRIVATE, messageToBeFiltered.getOrcidProfile() .retrieveOrcidPath()); decision = TreeCleaningDecision.IGNORE; } } catch (AccessControlException e) { // private email can't be read, do nothing here } } // if we have a source, and that source can read // limited, also return the private things they own // Applies to ExternalIdentifier, Keyword, // ResearcherUrl, OtherName and anything in the future // that implements PrivateVisibleToSource if (sourceId != null) if (PrivateVisibleToSource.class.isAssignableFrom(clazz) && visibilitySet.contains(Visibility.LIMITED)) { Source source = ((PrivateVisibleToSource) obj).getSource(); if (source != null) { if (sourceId.equals(source.retrieveSourcePath())) { decision = TreeCleaningDecision.IGNORE; } } } if (TreeCleaningDecision.DEFAULT.equals(decision)) { if (WorkContributors.class.isAssignableFrom(clazz)) { decision = TreeCleaningDecision.IGNORE; } else if (VisibilityType.class.isAssignableFrom(clazz)) { VisibilityType visibilityType = (VisibilityType) obj; if ((visibilityType.getVisibility() == null || !visibilitySet.contains(visibilityType.getVisibility()))) { decision = TreeCleaningDecision.CLEANING_REQUIRED; } } } } return decision; } }); OrcidProfile orcidProfile = messageToBeFiltered.getOrcidProfile(); if (orcidProfile != null) { orcidProfile.setOrcidInternal(null); } LOGGER.debug("Finished filtering message: " + messageIdForLog); return messageToBeFiltered; } } private Authentication getAuthentication() { SecurityContext context = SecurityContextHolder.getContext(); if (context != null && context.getAuthentication() != null) { return context.getAuthentication(); } return null; } private String getMessageIdForLog(OrcidMessage messageToBeFiltered) { String messageIdForLog = "unknown"; OrcidSearchResults orcidSearchResults = messageToBeFiltered.getOrcidSearchResults(); OrcidProfile orcidProfile = messageToBeFiltered.getOrcidProfile(); if (orcidSearchResults != null) { messageIdForLog = "orcid-search-results"; } else if (orcidProfile != null) { OrcidIdentifier orcidIdentifier = orcidProfile.getOrcidIdentifier(); if (orcidIdentifier != null) { messageIdForLog = orcidIdentifier.getPath(); } Orcid orcid = orcidProfile.getOrcid(); if (orcid != null) { messageIdForLog = orcid.getValue(); } } return messageIdForLog; } }