/** * ============================================================================= * * 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; import java.security.AccessControlException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import javax.xml.datatype.XMLGregorianCalendar; import org.apache.commons.lang.StringUtils; import org.orcid.core.exception.OrcidDeprecatedException; import org.orcid.core.manager.ProfileEntityCacheManager; import org.orcid.core.manager.ProfileEntityManager; import org.orcid.core.oauth.OrcidOAuth2Authentication; import org.orcid.core.oauth.OrcidOauth2TokenDetailService; import org.orcid.jaxb.model.message.OrcidIdentifier; import org.orcid.jaxb.model.message.OrcidMessage; import org.orcid.jaxb.model.message.ScopePathType; import org.orcid.jaxb.model.message.Visibility; import org.orcid.persistence.dao.ProfileDao; import org.orcid.persistence.jpa.entities.OrcidOauth2TokenDetail; import org.orcid.persistence.jpa.entities.ProfileEntity; import org.orcid.utils.DateUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.stereotype.Component; /** * @author Declan Newman (declan) Date: 27/04/2012 */ @Component("defaultPermissionChecker") public class DefaultPermissionChecker implements PermissionChecker { @Value("${org.orcid.core.token.write_validity_seconds:3600}") private int writeValiditySeconds; @Resource(name = "profileEntityManager") private ProfileEntityManager profileEntityManager; @Resource private ProfileDao profileDao; @Value("${org.orcid.core.baseUri}") private String baseUrl; @Resource private OrcidOauth2TokenDetailService orcidOauthTokenDetailService; @Resource(name = "profileEntityCacheManager") ProfileEntityCacheManager profileEntityCacheManager; /** * Check the permissions for the given * {@link org.springframework.security.core.Authentication} object and the * scopes defined in the required scopes * * @param authentication * The authentication object associated with this session * @param requiredScope * the scopes required to perform the requested operation * @param orcid * the orcid passed into the request. This is for requests, such * as a GET /1234-1234-1234-1234/orcid-bio * @param orcidMessage * the {@link org.orcid.jaxb.model.message.OrcidMessage} that has * been sent as part of this request. This will only apply to * PUTs and POSTs */ @Override public void checkPermissions(Authentication authentication, ScopePathType requiredScope, String orcid, OrcidMessage orcidMessage) { if (StringUtils.isNotBlank(orcid) && orcidMessage != null && orcidMessage.getOrcidProfile() != null && (orcidMessage.getOrcidProfile().getOrcidIdentifier() == null || StringUtils.isBlank(orcidMessage.getOrcidProfile().getOrcidIdentifier().getPath()))) { orcidMessage.getOrcidProfile().setOrcidIdentifier(orcid); } performPermissionChecks(authentication, requiredScope, orcid, orcidMessage); } /** * Check the permissions for the given * {@link org.springframework.security.core.Authentication} object and the * scopes defined in the required scopes * * @param authentication * The authentication object associated with this session * @param requiredScope * the scopes required to perform the requested operation * @param orcidMessage * the {@link org.orcid.jaxb.model.message.OrcidMessage} that has * been sent as part of this request. This will only apply to * PUTs and POSTs */ @Override public void checkPermissions(Authentication authentication, ScopePathType requiredScope, OrcidMessage orcidMessage) { performPermissionChecks(authentication, requiredScope, null, orcidMessage); } /** * Check the permissions for the given * {@link org.springframework.security.core.Authentication} object and the * scopes defined in the required scopes * * @param authentication * The authentication object associated with this session * @param requiredScope * the scopes required to perform the requested operation * @param orcid * the orcid passed into the request. This is for requests, such * as a GET /1234-1234-1234-1234/orcid-bio */ @Override public void checkPermissions(Authentication authentication, ScopePathType requiredScope, String orcid) { performPermissionChecks(authentication, requiredScope, orcid, null); } /** * Obtain the current users' permission and return the * {@link org.orcid.jaxb.model.message.Visibility} array containing those * * @param authentication * the object containing the user's security information * @return the {@alink Visibility} array of the current user */ @Override public Set<Visibility> obtainVisibilitiesForAuthentication(Authentication authentication, ScopePathType requiredScope, OrcidMessage orcidMessage) { Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); if (authoritiesHasRole(authorities, "ROLE_SYSTEM")) { return new HashSet<Visibility>(Arrays.asList(Visibility.SYSTEM)); } else if (OrcidOAuth2Authentication.class.isAssignableFrom(authentication.getClass())) { OrcidOAuth2Authentication auth2Authentication = (OrcidOAuth2Authentication) authentication; Set<Visibility> visibilities = getVisibilitiesForOauth2Authentication(auth2Authentication, orcidMessage, requiredScope); return visibilities; } else { throw new IllegalArgumentException("Cannot obtain authentication details from " + authentication); } } private Set<Visibility> getVisibilitiesForOauth2Authentication(OAuth2Authentication oAuth2Authentication, OrcidMessage orcidMessage, ScopePathType requiredScope) { Set<Visibility> visibilities = new HashSet<Visibility>(); visibilities.add(Visibility.PUBLIC); String orcid = orcidMessage.getOrcidProfile().getOrcidIdentifier().getPath(); // Check the scopes and it will throw an an AccessControlException if // the correct scope is not found. This // effectively means that the user can only see the public data try { checkScopes(oAuth2Authentication, requiredScope); } catch (AccessControlException e) { return visibilities; } // If the user has granted permission to the client and the orcid that // has been requested is that of the user // we can allow for access of protected data if (!oAuth2Authentication.isClientOnly() && oAuth2Authentication.getPrincipal() != null && ProfileEntity.class.isAssignableFrom(oAuth2Authentication.getPrincipal().getClass())) { ProfileEntity principal = (ProfileEntity) oAuth2Authentication.getPrincipal(); visibilities.add(Visibility.REGISTERED_ONLY); if (principal != null && principal.getId().equals(orcid)) { Set<String> requestedScopes = oAuth2Authentication.getOAuth2Request().getScope(); for (String scope : requestedScopes) { if (ScopePathType.hasStringScope(scope, requiredScope)) { visibilities.add(Visibility.LIMITED); break; } } } // This is a client credential authenticated client. If the profile // was created using this client and it // hasn't been claimed, it's theirs to read } else if (oAuth2Authentication.isClientOnly()) { OAuth2Request authorizationRequest = oAuth2Authentication.getOAuth2Request(); String clientId = authorizationRequest.getClientId(); String sponsorOrcid = getSponsorOrcid(orcidMessage); if (StringUtils.isNotBlank(sponsorOrcid) && clientId.equals(sponsorOrcid) && !orcidMessage.getOrcidProfile().getOrcidHistory().isClaimed()) { visibilities.add(Visibility.LIMITED); visibilities.add(Visibility.PRIVATE); } } return visibilities; } private String getSponsorOrcid(OrcidMessage orcidMessage) { if (orcidMessage != null && orcidMessage.getOrcidProfile() != null && orcidMessage.getOrcidProfile().getOrcidHistory() != null && orcidMessage.getOrcidProfile().getOrcidHistory().getSource() != null) { return orcidMessage.getOrcidProfile().getOrcidHistory().getSource().retrieveSourcePath(); } else { return null; } } private void performPermissionChecks(Authentication authentication, ScopePathType requiredScope, String orcid, OrcidMessage orcidMessage) { // We can trust that this will return a not-null Authentication object Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); if (authoritiesHasRole(authorities, "ROLE_SYSTEM")) { return; } else if (OAuth2Authentication.class.isAssignableFrom(authentication.getClass())) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication; checkScopes(oAuth2Authentication, requiredScope); performSecurityChecks(oAuth2Authentication, requiredScope, orcidMessage, orcid); } else { throw new AccessControlException("Cannot access method with authentication type " + authentication != null ? authentication.toString() : ", as it's null!"); } } private void checkScopes(OAuth2Authentication oAuth2Authentication, ScopePathType requiredScope) { OAuth2Request authorizationRequest = oAuth2Authentication.getOAuth2Request(); Set<String> requestedScopes = authorizationRequest.getScope(); if (requiredScope.isUserGrantWriteScope()) { OrcidOAuth2Authentication orcidOauth2Authentication = (OrcidOAuth2Authentication) oAuth2Authentication; String activeToken = orcidOauth2Authentication.getActiveToken(); if (activeToken != null) { OrcidOauth2TokenDetail tokenDetail = orcidOauthTokenDetailService.findNonDisabledByTokenValue(activeToken); if (removeUserGrantWriteScopePastValitity(tokenDetail)) { throw new AccessControlException("Write scopes for this token have expired "); } } } if (!hasRequiredScope(requestedScopes, requiredScope)) { throw new AccessControlException("Insufficient or wrong scope " + requestedScopes); } } /* * Remove UserGrantWriteScope past the specified validity, returns true if * modified false otherwise */ public boolean removeUserGrantWriteScopePastValitity(OrcidOauth2TokenDetail tokenDetail) { boolean scopeRemoved = false; if (tokenDetail != null && tokenDetail.getScope() != null) { // Clean the scope if it is not a persistent token if (!tokenDetail.isPersistent()) { Set<String> scopes = OAuth2Utils.parseParameterList(tokenDetail.getScope()); List<String> removeScopes = new ArrayList<String>(); for (String scope : scopes) { if (scope != null && !scope.isEmpty()) { ScopePathType scopePathType = ScopePathType.fromValue(scope); if (scopePathType.isUserGrantWriteScope()) { Date now = new Date(); if (now.getTime() > tokenDetail.getDateCreated().getTime() + (writeValiditySeconds * 1000)) { removeScopes.add(scope); scopeRemoved = true; } } } } if (scopeRemoved) { for (String scope : removeScopes) scopes.remove(scope); tokenDetail.setScope(OAuth2Utils.formatParameterList(scopes)); orcidOauthTokenDetailService.saveOrUpdate(tokenDetail); return true; } } } return false; } private void performSecurityChecks(OAuth2Authentication oAuth2Authentication, ScopePathType requiredScope, OrcidMessage orcidMessage, String orcid) { if (oAuth2Authentication.isClientOnly()) { performClientChecks(oAuth2Authentication, requiredScope, orcidMessage, orcid); } else { performUserChecks(oAuth2Authentication, requiredScope, orcidMessage, orcid); } } private void performUserChecks(OAuth2Authentication oAuth2Authentication, ScopePathType requiredScope, OrcidMessage orcidMessage, String orcid) { ProfileEntity principal = (ProfileEntity) oAuth2Authentication.getPrincipal(); String userOrcid = principal.getId(); if (orcidMessage != null && orcidMessage.getOrcidProfile() != null && orcidMessage.getOrcidProfile().getOrcidIdentifier() != null && StringUtils.isNotBlank(orcid)) { String messageOrcid = orcidMessage.getOrcidProfile().getOrcidIdentifier().getPath(); // First check that this is a valid call. If these don't match then // the request is invalid if (!messageOrcid.equals(orcid)) { throw new IllegalArgumentException("The ORCID in the body and the URI do not match. Body ORCID: " + messageOrcid + " URI ORCID: " + orcid + " do NOT match."); } } // Is this the owner making the call? If it is, then let 'em on // through if (userOrcid.equals(orcid)) { return; } else { if(profileDao.isProfileDeprecated(orcid)) { ProfileEntity entity = profileEntityCacheManager.retrieve(orcid); Map<String, String> params = new HashMap<String, String>(); StringBuffer primary = new StringBuffer(baseUrl).append("/").append(entity.getPrimaryRecord().getId()); params.put(OrcidDeprecatedException.ORCID, primary.toString()); if(entity.getDeprecatedDate() != null) { XMLGregorianCalendar calendar = DateUtils.convertToXMLGregorianCalendar(entity.getDeprecatedDate()); params.put(OrcidDeprecatedException.DEPRECATED_DATE, calendar.toString()); } throw new OrcidDeprecatedException(params); } } throw new AccessControlException("You do not have the required permissions."); } private void performClientChecks(OAuth2Authentication oAuth2Authentication, ScopePathType requiredScope, OrcidMessage orcidMessage, String orcid) { OAuth2Request authorizationRequest = oAuth2Authentication.getOAuth2Request(); // If we have an ORCID in the request, we assume that this is intended // as an update if (orcidMessage != null && orcidMessage.getOrcidProfile() != null && StringUtils.isNotBlank(orcid)) { OrcidIdentifier orcidOb = orcidMessage.getOrcidProfile().getOrcidIdentifier(); String messageOrcid = orcidOb != null ? orcidOb.getPath() : orcid; if (StringUtils.isNotBlank(messageOrcid) && !orcid.equals(messageOrcid)) { throw new IllegalArgumentException("The ORCID in the body and the URI do NOT match. Body ORCID: " + messageOrcid + " URI ORCID: " + orcid + " do NOT match."); } profileEntityCacheManager.retrieve(messageOrcid); if (!profileEntityManager.existsAndNotClaimedAndBelongsTo(messageOrcid, authorizationRequest.getClientId())) { throw new AccessControlException("You cannot update this profile as it has been claimed, or you are not the owner."); } } } private boolean authoritiesHasRole(Collection<? extends GrantedAuthority> authorities, String role) { for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equalsIgnoreCase(role)) { return true; } } return false; } private boolean hasRequiredScope(Set<String> requestedScopes, ScopePathType requiredScope) { for (String requestedScope : requestedScopes) { if (ScopePathType.hasStringScope(requestedScope, requiredScope)) { return true; } if (requiredScope.isReadOnlyScope()) { // If read only (limited or otherwise) then let it through it // the user has /read-public, and let the visibility filter take // care of it. if (ScopePathType.hasStringScope(requestedScope, ScopePathType.READ_PUBLIC)) { return true; } } } return false; } public void setOrcidOauthTokenDetailService(OrcidOauth2TokenDetailService orcidOauthTokenDetailService) { this.orcidOauthTokenDetailService = orcidOauthTokenDetailService; } }