/** * Copyright (c) 2011-2014, OpenIoT * * This library is free software; you can redistribute it and/or * modify it either under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * (the "LGPL"). If you do not alter this * notice, a recipient may use your version of this file under the LGPL. * * You should have received a copy of the LGPL along with this library * in the file COPYING-LGPL-2.1; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY * OF ANY KIND, either express or implied. See the LGPL for * the specific language governing rights and limitations. * * Contact: OpenIoT mailto: info@openiot.eu */ package org.openiot.security.client; import static org.openiot.security.client.SecurityConstants.CALLER_ACCESS_TOKEN; import static org.openiot.security.client.SecurityConstants.CALLER_CLIENT_ID; import static org.openiot.security.client.SecurityConstants.ERROR; import static org.openiot.security.client.SecurityConstants.ROLE_PERMISSIONS; import static org.openiot.security.client.SecurityConstants.TARGET_CLIENT_ID; import static org.openiot.security.client.SecurityConstants.USER_ACCESS_TOKEN; import static org.openiot.security.client.SecurityConstants.*; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.permission.PermissionResolver; import org.apache.shiro.authz.permission.WildcardPermissionResolver; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.subject.PrincipalCollection; import org.pac4j.core.exception.HttpCommunicationException; import org.pac4j.oauth.client.BaseOAuth20Client; import org.pac4j.oauth.profile.JsonHelper; import org.pac4j.oauth.profile.casoauthwrapper.CasOAuthWrapperProfile; import org.scribe.model.OAuthConstants; import org.scribe.model.ProxyOAuthRequest; import org.scribe.model.Response; import org.scribe.model.Verb; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; /** * This class provides authorization information to the clients. The permissions and roles are * obtained for the tokens on services as follows: * <ul> * <li>If OAuthorizationCredentials does not have caller credentials, then the authorization * information is retrieved and consulted for its token on its service (specified by clientId)</li> * <li>If OAuthorizationCredentials has caller credentials, but the caller does not have a caller * (i.e., chain of 1 credentials), then the authorization information is retrieved and consulted for * the caller's token on the current service (specified by clientId) unless the target service is * explicitly specified</li> * <li>If OAuthorizationCredentials has a caller credentials, which also has a caller (i.e., chain * of 3 credentials), then the authorization information is retrieved and consulted for the the last * caller's token on the current service (specified by clientId) unless the target service is * explicitly specified</li> * </ul> * If the target service is different than the current service, the user must have the required * permission for retrieving authorization information on the target service. * * @author Mehdi Riahi * */ public class AuthorizationManager implements ClearCacheListener { // TODO: use the subject's session for caching authorization data and back the session store by // Ehcache. private static Logger logger = LoggerFactory.getLogger(AuthorizationManager.class); private Cache<CacheKey, Map<String, Set<Permission>>> cacheManager; private BaseOAuth20Client<?> client; private boolean cachingEnabled = false; private String permissionsURL; private PermissionResolver permissionResolver = new WildcardPermissionResolver(); public AuthorizationManager() { } public void setCacheManager(CacheManager cacheManager) { if (cacheManager != null) { logger.debug("Setting the cache manager to {}", cacheManager.getClass().getCanonicalName()); this.cacheManager = cacheManager.<CacheKey, Map<String, Set<Permission>>> getCache("AuthorizationManager-Cache"); cachingEnabled = true; } } public void setPermissionsURL(String permissionsURL) { this.permissionsURL = permissionsURL; } public boolean isCachingEnabled() { return cachingEnabled; } public void setClient(BaseOAuth20Client<?> client) { this.client = client; } /** * Sends a request to the server to check if the token is expired. * * @param credentials * @return the expired token or <code>null</code> if non of the tokens in * <code>credentials</code> are expired */ public String getExpiredAccessToken(OAuthorizationCredentials credentials) { if (cachingEnabled) cacheManager.remove(new CacheKey(credentials.getClientId(), credentials)); try { getAuthorizationInfo(credentials, credentials.getClientId()); } catch (AccessTokenExpiredException e) { return e.getToken(); } return null; } void reset(String token) { clearCacheForToken(token); } public boolean hasPermission(String permStr, OAuthorizationCredentials credentials) { if (credentials == null) return false; return hasPermission(permStr, credentials.getClientId(), credentials); } public boolean hasPermission(String permStr, String targetClientId, OAuthorizationCredentials credentials) { if (credentials == null) return false; Permission perm = permissionResolver.resolvePermission(permStr); Map<String, Set<Permission>> authorizationInfo = getAuthorizationInfo(credentials, targetClientId == null ? credentials.getClientId() : targetClientId); boolean hasPerm = false; for (Set<Permission> permSet : authorizationInfo.values()) { if (hasPerm) break; for (Permission permission : permSet) if (permission.implies(perm)) { hasPerm = true; break; } } return hasPerm; } public boolean hasRole(String role, OAuthorizationCredentials credentials) { if (credentials == null) return false; return hasRole(role, credentials.getClientId(), credentials); } public boolean hasRole(String role, String targetClientId, OAuthorizationCredentials credentials) { if (credentials == null) return false; Map<String, Set<Permission>> authorizationInfo = getAuthorizationInfo(credentials, targetClientId == null ? credentials.getClientId() : targetClientId); return authorizationInfo.containsKey(role); } protected Map<String, Set<Permission>> getAuthorizationInfo(final OAuthorizationCredentials credentials, final String targetClientId) { Map<String, Set<Permission>> authorizationInfo = null; CacheKey key = new CacheKey(targetClientId, credentials); if (cachingEnabled) authorizationInfo = cacheManager.get(key); if (authorizationInfo == null) { authorizationInfo = getAuthorizationInfoInternal(credentials, targetClientId); if (cachingEnabled) cacheManager.put(key, authorizationInfo); } return authorizationInfo; } protected Map<String, Set<Permission>> getAuthorizationInfoInternal(final OAuthorizationCredentials credentials, final String targetClientId) throws AccessTokenExpiredException { Map<String, Set<Permission>> map = new HashMap<String, Set<Permission>>(); final String body = sendRequestForPermissions(credentials, targetClientId); JsonNode json = JsonHelper.getFirstNode(body); if (json != null) { JsonNode errorNode = json.get(ERROR); if (errorNode != null) { String errorMsg = errorNode.asText(); logger.info("Error returned: {}", errorMsg); if (errorMsg.startsWith(EXPIRED_ACCESS_TOKEN)) { String token = credentials.getAccessToken(); if (errorMsg.endsWith("_for_caller")) token = credentials.getCallerCredentials().getAccessToken(); else if (errorMsg.endsWith("_for_user")) if (credentials.getCallerCredentials().getCallerCredentials() == null) token = credentials.getCallerCredentials().getAccessToken(); else token = credentials.getCallerCredentials().getCallerCredentials().getAccessToken(); throw new AccessTokenExpiredException(token, errorMsg); } } else { json = json.get(ROLE_PERMISSIONS); if (json != null) { final Iterator<JsonNode> nodes = json.iterator(); while (nodes.hasNext()) { for (Iterator<Entry<String, JsonNode>> fields = nodes.next().fields(); fields.hasNext();) { Entry<String, JsonNode> next = fields.next(); logger.debug("next role: {}", next.getKey()); final HashSet<Permission> permissionsSet = new HashSet<Permission>(); map.put(next.getKey(), permissionsSet); Iterator<JsonNode> permIter = next.getValue().iterator(); while (permIter.hasNext()) { json = permIter.next(); String permission = json.asText(); permissionsSet.add(permissionResolver.resolvePermission(permission)); logger.debug("next permission: {}", permission); } } } } } } return map; } protected String sendRequestForPermissions(final OAuthorizationCredentials credentials, final String targetClientId) { logger.debug("accessToken : {} / permissionsUrl : {}", credentials.getAccessToken(), permissionsURL); final long t0 = System.currentTimeMillis(); final ProxyOAuthRequest request = new ProxyOAuthRequest(Verb.GET, permissionsURL, client.getConnectTimeout(), client.getReadTimeout(), client.getProxyHost(), client.getProxyPort()); request.addQuerystringParameter(OAuthConstants.CLIENT_ID, credentials.getClientId()); request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN, credentials.getAccessToken()); String userToken = credentials.getAccessToken(); final OAuthorizationCredentials callerCredentials = credentials.getCallerCredentials(); if (callerCredentials != null) { final OAuthorizationCredentials userCredentials = callerCredentials.getCallerCredentials(); if (userCredentials != null) { userToken = userCredentials.getAccessToken(); request.addQuerystringParameter(USER_CLIENT_ID, userCredentials.getClientId()); request.addQuerystringParameter(CALLER_CLIENT_ID, callerCredentials.getClientId()); request.addQuerystringParameter(CALLER_ACCESS_TOKEN, callerCredentials.getAccessToken()); } else { userToken = callerCredentials.getAccessToken(); request.addQuerystringParameter(USER_CLIENT_ID, callerCredentials.getClientId()); } } request.addQuerystringParameter(USER_ACCESS_TOKEN, userToken); request.addQuerystringParameter(TARGET_CLIENT_ID, targetClientId); final Response response = request.send(); final int code = response.getCode(); final String body = response.getBody(); final long t1 = System.currentTimeMillis(); logger.debug("Request took : " + (t1 - t0) + " ms for : " + permissionsURL); logger.debug("response code : {} / response body : {}", code, body); if (code != 200) { logger.error("Failed to get permissions, code : " + code + " / body : " + body); throw new HttpCommunicationException(code, body); } return body; } @Override public void clearCache(PrincipalCollection principals) { if (cachingEnabled) { if (principals == null) { logger.debug("Clearing cache"); cacheManager.clear(); } else { final CasOAuthWrapperProfile profile = principals.oneByType(CasOAuthWrapperProfile.class); String accessToken = profile.getAccessToken(); logger.debug("Clearing cache for accessToken: {} ", accessToken); clearCacheForToken(accessToken); } } } private void clearCacheForToken(String accessToken) { Set<CacheKey> keys = cacheManager.keys(); for (CacheKey key : keys) { if (key.credentials.containsToken(accessToken)) cacheManager.remove(key); } } private static class CacheKey { String targetClientId; OAuthorizationCredentials credentials; public CacheKey(String targetClientId, OAuthorizationCredentials credentials) { this.targetClientId = targetClientId; this.credentials = credentials; } @Override public int hashCode() { HashCodeBuilder builder = new HashCodeBuilder(); builder.append(targetClientId); builder.append(credentials); return builder.build(); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } } }