/**
* =============================================================================
*
* 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.aop;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.orcid.core.oauth.OrcidOAuth2Authentication;
import org.orcid.core.security.PermissionChecker;
import org.orcid.core.security.visibility.filter.VisibilityFilter;
import org.orcid.jaxb.model.message.ExternalIdentifiers;
import org.orcid.jaxb.model.message.GivenNames;
import org.orcid.jaxb.model.message.Keywords;
import org.orcid.jaxb.model.message.OrcidBio;
import org.orcid.jaxb.model.message.OrcidMessage;
import org.orcid.jaxb.model.message.OrcidProfile;
import org.orcid.jaxb.model.message.OtherNames;
import org.orcid.jaxb.model.message.PersonalDetails;
import org.orcid.jaxb.model.message.ResearcherUrls;
import org.orcid.jaxb.model.message.ScopePathType;
import org.orcid.jaxb.model.message.Visibility;
import org.orcid.jaxb.model.message.VisibilityType;
import org.orcid.jaxb.model.notification_v2.Notification;
import org.orcid.jaxb.model.record_v2.Activity;
import org.orcid.persistence.dao.OrcidOauth2TokenDetailDao;
import org.orcid.pojo.ajaxForm.PojoUtil;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.stereotype.Component;
/**
* @author Declan Newman (declan) Date: 16/03/2012
*/
@Deprecated
@Aspect
@Component
@Order(100)
public class OrcidApiAuthorizationSecurityAspect {
public static final String CLIENT_ID = "client_id";
@Resource
private OrcidOauth2TokenDetailDao orcidOauth2TokenDetailDao;
@Resource(name = "visibilityFilter")
private VisibilityFilter visibilityFilter;
@Resource(name = "defaultPermissionChecker")
private PermissionChecker permissionChecker;
public void setOrcidOauth2TokenDetailDao(OrcidOauth2TokenDetailDao orcidOauth2TokenDetailDao) {
this.orcidOauth2TokenDetailDao = orcidOauth2TokenDetailDao;
}
@Before("@annotation(accessControl) && (args(uriInfo ,orcid, orcidMessage))")
public void checkPermissionsWithAll(AccessControl accessControl, UriInfo uriInfo, String orcid, OrcidMessage orcidMessage) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid, orcidMessage);
}
@Before("@annotation(accessControl) && (args(uriInfo, orcidMessage))")
public void checkPermissionsWithOrcidMessage(AccessControl accessControl, UriInfo uriInfo, OrcidMessage orcidMessage) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcidMessage);
}
@Before("@annotation(accessControl) && args(orcid)")
public void checkPermissionsWithOrcid(AccessControl accessControl, String orcid) {
Authentication auth = getAuthentication();
boolean allowAnonymousCall = allowAnonymousAccess(auth, accessControl);
if(!allowAnonymousCall) {
permissionChecker.checkPermissions(auth, accessControl.requiredScope(), orcid);
}
}
@Before("@annotation(accessControl) && args(orcid, id)")
public void checkPermissionsWithLongId(AccessControl accessControl, String orcid, Long id) {
Authentication auth = getAuthentication();
boolean allowAnonymousCall = allowAnonymousAccess(auth, accessControl);
if(!allowAnonymousCall) {
permissionChecker.checkPermissions(auth, accessControl.requiredScope(), orcid);
}
}
@Before("@annotation(accessControl) && args(orcid, id)")
public void checkPermissionsWithId(AccessControl accessControl, String orcid, String id) {
Authentication auth = getAuthentication();
boolean allowAnonymousCall = allowAnonymousAccess(auth, accessControl);
if(!allowAnonymousCall) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid);
}
}
@Before("@annotation(accessControl) && args(uriInfo, orcid, notification)")
public void checkPermissionsWithNotification(AccessControl accessControl, UriInfo uriInfo, String orcid, Notification notification) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid);
}
@Before("@annotation(accessControl) && args(orcid, activity)")
public void checkPermissionsWithWork(AccessControl accessControl, String orcid, Activity activity) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid);
}
@Before("@annotation(accessControl) && args(orcid, putCode, activity)")
public void checkPermissionsWithWork(AccessControl accessControl, String orcid, String putCode, Activity activity) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid);
}
@Before("@annotation(accessControl) && args(uriInfo, orcid, webhookUri)")
public void checkPermissionsWithOrcidAndWebhookUri(AccessControl accessControl, UriInfo uriInfo, String orcid, String webhookUri) {
permissionChecker.checkPermissions(getAuthentication(), accessControl.requiredScope(), orcid);
}
@AfterReturning(pointcut = "@annotation(accessControl)", returning = "response")
public void visibilityResponseFilter(Response response, AccessControl accessControl) {
if(accessControl.requestComesFromInternalApi()) {
return;
}
Object entity = response.getEntity();
if (entity != null && OrcidMessage.class.isAssignableFrom(entity.getClass())) {
OrcidMessage orcidMessage = (OrcidMessage) entity;
//If it is search results, don't filter them, just return them
if(orcidMessage.getOrcidSearchResults() != null) {
return;
}
// get the client id
Object authentication = getAuthentication();
Set<Visibility> visibilities = new HashSet<Visibility>();
if(allowAnonymousAccess((Authentication)authentication, accessControl)) {
visibilities.add(Visibility.PUBLIC);
} else {
visibilities = permissionChecker.obtainVisibilitiesForAuthentication(getAuthentication(), accessControl.requiredScope(), orcidMessage);
}
//If the message contains a bio, and the given name is filtered, restore it as an empty space
boolean setEmptyGivenNameIfFiltered = false;
if(orcidMessage.getOrcidProfile() != null) {
if(orcidMessage.getOrcidProfile() != null && orcidMessage.getOrcidProfile().getOrcidBio() != null) {
setEmptyGivenNameIfFiltered = true;
}
}
ScopePathType requiredScope = accessControl.requiredScope();
// If the required scope is */read-limited or */update
if (isUpdateOrReadScope(requiredScope)) {
// If the authentication contains a client_id, use it to check
// if it should be able to
if (OrcidOAuth2Authentication.class.isAssignableFrom(authentication.getClass())){
OrcidOAuth2Authentication orcidAuth = (OrcidOAuth2Authentication) getAuthentication();
OAuth2Request authorization = orcidAuth.getOAuth2Request();
String clientId = authorization.getClientId();
// #1: Get the user orcid
String userOrcid = getUserOrcidFromOrcidMessage(orcidMessage);
// #2: Evaluate the scope to know which field to filter
boolean allowWorks = false;
boolean allowFunding = false;
boolean allowAffiliations = false;
// Get the update equivalent scope, if it is reading, but,
// doesnt have the read permissions, check if it have the
// update permissions
ScopePathType equivalentUpdateScope = getEquivalentUpdateScope(requiredScope);
if (requiredScope.equals(ScopePathType.READ_LIMITED)) {
if (hasScopeEnabled(clientId, userOrcid, ScopePathType.ORCID_WORKS_READ_LIMITED.getContent(), ScopePathType.ORCID_WORKS_UPDATE.getContent()))
allowWorks = true;
if (hasScopeEnabled(clientId, userOrcid, ScopePathType.FUNDING_READ_LIMITED.getContent(), ScopePathType.FUNDING_UPDATE.getContent()))
allowFunding = true;
if (hasScopeEnabled(clientId, userOrcid, ScopePathType.AFFILIATIONS_READ_LIMITED.getContent(), ScopePathType.AFFILIATIONS_UPDATE.getContent()))
allowAffiliations = true;
} else if (requiredScope.equals(ScopePathType.ORCID_WORKS_UPDATE) || requiredScope.equals(ScopePathType.ORCID_WORKS_READ_LIMITED)) {
// Check if the member have the update or read scope on
// works
if (hasScopeEnabled(clientId, userOrcid, requiredScope.getContent(), equivalentUpdateScope == null ? null : equivalentUpdateScope.getContent()))
// If so, allow him to see private works
allowWorks = true;
} else if (requiredScope.equals(ScopePathType.FUNDING_UPDATE) || requiredScope.equals(ScopePathType.FUNDING_READ_LIMITED)) {
// Check if the member have the update or read scope on
// funding
if (hasScopeEnabled(clientId, userOrcid, requiredScope.getContent(), equivalentUpdateScope == null ? null : equivalentUpdateScope.getContent()))
// If so, allow him to see private funding
allowFunding = true;
} else if (requiredScope.equals(ScopePathType.AFFILIATIONS_UPDATE) || requiredScope.equals(ScopePathType.AFFILIATIONS_READ_LIMITED)) {
// Check if the member have the update or read scope on
// affiliations
if (hasScopeEnabled(clientId, userOrcid, requiredScope.getContent(), equivalentUpdateScope == null ? null : equivalentUpdateScope.getContent()))
// If so, allow him to see private affiliations
allowAffiliations = true;
}
visibilityFilter.filter(orcidMessage, clientId, allowWorks, allowFunding, allowAffiliations,
visibilities.toArray(new Visibility[visibilities.size()]));
} else {
visibilityFilter.filter(orcidMessage, null, false, false, false, visibilities.toArray(new Visibility[visibilities.size()]));
}
} else {
visibilityFilter.filter(orcidMessage, null, false, false, false, visibilities.toArray(new Visibility[visibilities.size()]));
}
//This applies for given names that were filtered because of the new visibility field applied on them
//If the given name was set at the beginning and now is filtered, it means we should restore it as an empty field
if(setEmptyGivenNameIfFiltered) {
if(orcidMessage.getOrcidProfile() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio() == null) {
orcidMessage.getOrcidProfile().setOrcidBio(new OrcidBio());
}
if(orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails() == null) {
orcidMessage.getOrcidProfile().getOrcidBio().setPersonalDetails(new PersonalDetails());
}
}
}
//Filter given or family names visibility
if(orcidMessage.getOrcidProfile() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().getGivenNames() != null) {
orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().getGivenNames().setVisibility(null);
} else {
//Null given names could break client integrations, so, lets return an empty string
GivenNames empty = new GivenNames();
empty.setContent(StringUtils.EMPTY);
orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().setGivenNames(empty);
}
if(orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().getFamilyName() != null) {
orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().getFamilyName().setVisibility(null);
}
}
}
}
//replace section visibilities now we may have filtered items
if(orcidMessage.getOrcidProfile() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio() != null) {
if(orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails() != null) {
OtherNames n = orcidMessage.getOrcidProfile().getOrcidBio().getPersonalDetails().getOtherNames();
if(n != null) {
n.setVisibility(getMostFromCollection(n.getOtherName()));
}
}
ExternalIdentifiers ids = orcidMessage.getOrcidProfile().getOrcidBio().getExternalIdentifiers();
if (ids != null){
ids.setVisibility(getMostFromCollection(ids.getExternalIdentifier()));
}
Keywords kws = orcidMessage.getOrcidProfile().getOrcidBio().getKeywords();
if (kws != null){
kws.setVisibility(getMostFromCollection(kws.getKeyword()));
}
ResearcherUrls urls = orcidMessage.getOrcidProfile().getOrcidBio().getResearcherUrls();
if (urls != null){
urls.setVisibility(getMostFromCollection(urls.getResearcherUrl()));
}
}
}
}
}
private Visibility getMostFromCollection(List<? extends VisibilityType> c){
Visibility most = Visibility.PUBLIC;
for (VisibilityType x : c){
if (x.getVisibility().isMoreRestrictiveThan(most))
most = x.getVisibility();
}
return most;
}
private String getUserOrcidFromOrcidMessage(OrcidMessage message) {
OrcidProfile profile = message.getOrcidProfile();
return profile.getOrcidIdentifier().getPath();
}
private boolean isUpdateOrReadScope(ScopePathType requiredScope) {
switch (requiredScope) {
case AFFILIATIONS_READ_LIMITED:
case AFFILIATIONS_UPDATE:
case FUNDING_READ_LIMITED:
case FUNDING_UPDATE:
case ORCID_BIO_READ_LIMITED:
case ORCID_BIO_UPDATE:
case ORCID_PATENTS_READ_LIMITED:
case ORCID_PATENTS_UPDATE:
case ORCID_PROFILE_READ_LIMITED:
case READ_LIMITED:
case ORCID_WORKS_READ_LIMITED:
case ORCID_WORKS_UPDATE:
return true;
default:
return false;
}
}
@Deprecated
public boolean hasScopeEnabled(String clientId, String userName, String scope, String equivalentScope) {
List<String> scopes = new ArrayList<String>();
scopes.add(scope);
if (equivalentScope != null)
scopes.add(equivalentScope);
return checkIfScopeIsAvailableForMember(clientId, userName, scopes);
}
private Authentication getAuthentication() {
SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) {
return context.getAuthentication();
} else {
throw new IllegalStateException("No security context found. This is bad!");
}
}
private ScopePathType getEquivalentUpdateScope(ScopePathType readScope) {
if (readScope != null)
switch (readScope) {
case AFFILIATIONS_READ_LIMITED:
return ScopePathType.AFFILIATIONS_UPDATE;
case FUNDING_READ_LIMITED:
return ScopePathType.FUNDING_UPDATE;
case ORCID_WORKS_READ_LIMITED:
return ScopePathType.ORCID_WORKS_UPDATE;
default:
return null;
}
return null;
}
private boolean allowAnonymousAccess(Authentication auth, AccessControl accessControl) {
boolean allowAnonymousAccess = false;
if(auth != null) {
for(GrantedAuthority grantedAuth : auth.getAuthorities()) {
if("ROLE_ANONYMOUS".equals(grantedAuth.getAuthority())) {
if(!accessControl.enableAnonymousAccess()) {
break;
}
allowAnonymousAccess = true;
break;
}
}
}
return allowAnonymousAccess;
}
/**
* Check if a member have a specific scope over a client
*
* @param clientId
* @param userName
* @param scopes
* @return true if the member have access to any of the specified scope on the specified user
* */
@Deprecated
private boolean checkIfScopeIsAvailableForMember(String clientId, String userName, List<String> requiredScopes) {
List<String> availableScopes = orcidOauth2TokenDetailDao.findAvailableScopesByUserAndClientId(clientId, userName);
for(String availableScope : availableScopes) {
String [] simpleScopes = availableScope.split(" ");
for(String simpleScope : simpleScopes) {
if(!PojoUtil.isEmpty(simpleScope)) {
ScopePathType scopePathType = ScopePathType.fromValue(simpleScope);
for(String requiredScope: requiredScopes) {
if(scopePathType.hasScope(requiredScope))
return true;
}
}
}
}
return false;
}
}