/******************************************************************************* * Copyright (c) 2012 GigaSpaces Technologies Ltd. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package org.cloudifysource.security; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.cloudifysource.dsl.internal.CloudifyConstants; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AuthorizationServiceException; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; /** * A custom PermissionEvaluator which performs permission decisions based on roles assignments and * authorization groups membership. * * @author noak * @since 2.3.0 */ public class CustomPermissionEvaluator implements PermissionEvaluator { private static final String LOCALCLOUD = "localcloud"; private static final String PERMISSION_TO_DEPLOY = "deploy"; private static final String PERMISSION_TO_VIEW = "view"; private static final String AUTH_GROUPS_DELIMITER = ","; private static final String ROLE_CLOUDADMIN = "ROLE_CLOUDADMINS"; private static final String ROLE_APPMANAGER = "ROLE_APPMANAGERS"; private static final String ROLE_VIEWER = "ROLE_VIEWERS"; private static final String SPRING_SECURITY_PROFILE = System.getenv(SecurityConstants.SPRING_ACTIVE_PROFILE_ENV_VAR); private final Logger logger = java.util.logging.Logger.getLogger(CustomPermissionEvaluator.class.getName()); /** * Checks if the current user should be granted the requested permission on the target object. * @param authentication The authentication object of the current user * @param targetDomainObject The target object the user is attempting to access * @param permission The permission requested on the target object (e.g. view, deploy) * @return boolean value - true if permission is granted, false otherwise. */ public boolean hasPermission(final Authentication authentication, final Object targetDomainObject, final Object permission) { return hasPermission(new CloudifyAuthorizationDetails(authentication), targetDomainObject, permission); } /** * Checks if the current user should be granted the requested permission on the target object. * @param authDetails The CloudifyAuthorizationDetails object of the current user * @param targetDomainObject The target object the user is attempting to access * @param permission The permission requested on the target object (e.g. view, deploy) * @return boolean value - true if permission is granted, false otherwise. * @throws IllegalArgumentException Indicates one or more of the passed arguments are null */ public boolean hasPermission(final CloudifyAuthorizationDetails authDetails, final Object targetDomainObject, final Object permission) throws IllegalArgumentException { if (StringUtils.isBlank(SPRING_SECURITY_PROFILE) || SPRING_SECURITY_PROFILE.contains(SecurityConstants.SPRING_PROFILE_NON_SECURE)) { //security is off return true; } if (authDetails == null) { throw new IllegalArgumentException("Null is not a valid value for CloudifyAuthorizationDetails"); } if (permission == null) { throw new IllegalArgumentException("Null is not a valid value for permission"); } if (logger.isLoggable(Level.FINE)) { logger.fine("Starting \"hasPermission\" for user: " + authDetails.getUsername()); if (authDetails.getRoles() == null) { logger.fine("with roles: null,"); } else { logger.fine("with roles: " + collectionToDelimitedString(authDetails.getRoles(), ",")); } if (authDetails.getAuthGroups() == null) { logger.fine("and with authGroups: null"); } else { logger.fine("and with authGroups: " + collectionToDelimitedString(authDetails.getAuthGroups(), ",")); } logger.fine("requested permission: " + permission == null ? "" : permission.toString()); logger.fine("on target authGroups: " + targetDomainObject == null ? "" : targetDomainObject.toString()); } boolean permissionGranted = false; String permissionName, targetAuthGroups; if (permission != null && !(permission instanceof String)) { throw new AuthorizationServiceException("Failed to verify permissions, invalid permission object type: " + permission.getClass().getName()); } permissionName = (String) permission; if (StringUtils.isBlank(permissionName)) { throw new AuthorizationServiceException("Failed to verify permissions, missing permission name"); } if (!permissionName.equalsIgnoreCase(PERMISSION_TO_VIEW) && !permissionName.equalsIgnoreCase(PERMISSION_TO_DEPLOY)) { throw new AuthorizationServiceException("Unsupported permission name: " + permissionName + ". valid permission names are: " + PERMISSION_TO_VIEW + ", " + PERMISSION_TO_DEPLOY); } if (targetDomainObject != null && !(targetDomainObject instanceof String)) { throw new AuthorizationServiceException("Failed to verify permissions, invalid authorization groups object" + " type: " + targetDomainObject.getClass().getName()); } if (targetDomainObject == null) { targetAuthGroups = ""; } else { targetAuthGroups = ((String) targetDomainObject).trim(); } if (hasRequiredRoles(authDetails, permissionName) && hasAuthGroupAccess(authDetails, targetAuthGroups, permissionName)) { permissionGranted = true; } return permissionGranted; } /** * Checks if the current user should be granted the requested permission on the target object. * This signature is currently not implemented. * @param authentication The authentication object of the current user * @param targetId The A unique identifier of the target object the user is attempting to access * @param targetType The type of the target object the user is attempting to access * @param permission The permission requested on the target object (e.g. view, deploy) * @return boolean value - true if permission is granted, false otherwise. */ public boolean hasPermission(final Authentication authentication, final Serializable targetId, final String targetType, final Object permission) { logger.warning("Evaluating expression using hasPermission unimplemented signature"); if (SecuredObjectTypes.AUTHORIZATION_GROUP.toString().equalsIgnoreCase(targetType)) { return hasPermission(authentication, targetId, permission); } else { throw new IllegalArgumentException("This object type cannot be secured: " + targetType); } } /** * Verifies the current user has the requested permission on the target object. * @param authDetails The CloudifyAuthorizationDetails object representing the current user authorization details * @param targetDomainObject The target object the user is attempting to access * @param permission The permission requested on the target object (e.g. view, deploy) */ public void verifyPermission(final CloudifyAuthorizationDetails authDetails, final Object targetDomainObject, final Object permission) { if (!hasPermission(authDetails, targetDomainObject, permission)) { throw new AccessDeniedException("User " + authDetails.getUsername() + " is not permitted to " + "access the target objects"); } } /** * Checks if the logged in user is allowed to access the target object, according to its roles. * @param authDetails The CloudifyAuthorizationDetails object of the logged in user. * @param permissionName permission requested (view, deploy, etc.) * @return true - access allowed, false - access denied. */ private boolean hasRequiredRoles(final CloudifyAuthorizationDetails authDetails, final String permissionName) { boolean relevantRoleFound = false; Collection<String> userRoles = authDetails.getRoles(); //TODO [noak] : This logic should be configurable if (permissionName.equalsIgnoreCase(PERMISSION_TO_VIEW)) { for (String role : userRoles) { if (ROLE_CLOUDADMIN.equalsIgnoreCase(role) || ROLE_APPMANAGER.equalsIgnoreCase(role) || ROLE_VIEWER.equalsIgnoreCase(role)) { relevantRoleFound = true; break; } } } else if (permissionName.equalsIgnoreCase(PERMISSION_TO_DEPLOY)) { for (String role : userRoles) { if (ROLE_CLOUDADMIN.equalsIgnoreCase(role) || ROLE_APPMANAGER.equalsIgnoreCase(role)) { relevantRoleFound = true; break; } } } if (!relevantRoleFound) { logger.log(Level.WARNING, "User " + authDetails.getUsername() + " is missing the required roles, access is " + "denied."); } return relevantRoleFound; } /** * Checks if the logged in user is allowed to access the target object, according to its authorization groups. * @param authDetails CloudifyAuthorizationDetails object of the logged in user * @param targetAuthGroupsStr Comma delimited string of the target object's authorization groups. * @param permissionName permission requested (view, deploy, etc.) * @return true - access allowed, false - access denied. */ private boolean hasAuthGroupAccess(final CloudifyAuthorizationDetails authDetails, final String targetAuthGroupsStr, final String permissionName) { boolean permissionGranted = false; //if the target object has no auth-groups: //if running on localcloud return true (it's probably a machine) //otherwise - only cloud admins can view it. if (StringUtils.isBlank(targetAuthGroupsStr)) { if (isLocalCloud()) { return true; } else { return (isCloudAdmin(authDetails.getRoles())); } } Collection<String> targetAuthGroups = splitAndTrimString(targetAuthGroupsStr, AUTH_GROUPS_DELIMITER); Collection<String> userAuthGroups = authDetails.getAuthGroups(); if (permissionName.equalsIgnoreCase(PERMISSION_TO_VIEW)) { if (hasPermissionToView(authDetails, targetAuthGroups)) { permissionGranted = true; logger.log(Level.FINE, "View permission granted for user " + authDetails.getUsername()); } else { logger.log(Level.WARNING, "Insufficient permissions. User " + authDetails.getUsername() + " is only " + "permitted to view groups: " + Arrays.toString(userAuthGroups.toArray(new String[userAuthGroups.size()]))); } } else if (permissionName.equalsIgnoreCase(PERMISSION_TO_DEPLOY)) { if (hasPermissionToDeploy(authDetails, targetAuthGroups)) { permissionGranted = true; logger.log(Level.INFO, "Deploy permission granted for user " + authDetails.getUsername()); } else { logger.log(Level.WARNING, "Insufficient permissions. User " + authDetails.getUsername() + " is only " + "permitted to deploy for groups: " + Arrays.toString(userAuthGroups.toArray(new String[userAuthGroups.size()]))); } } return permissionGranted; } /** * Checks if the current user is allowed to view the an object that has the specified authorization groups. * If the user has *any* of the target object's authorization groups - permission to view it is granted. * @param authDetails The CloudifyAuthorizationDetails object of the user who requests permission * @param requestedAuthGroups The authorization groups of the target object * @return boolean value - true if permission is granted, false otherwise. */ private boolean hasPermissionToView(final CloudifyAuthorizationDetails authDetails, final Collection<String> requestedAuthGroups) { return hasAnyAuthGroup(authDetails, requestedAuthGroups); } /** * Checks if the current user is allowed to view the an object that has the specified authorization groups. * If the user has *any* the authorization groups of the object - permission to view it is granted. * @param authDetails The CloudifyAuthorizationDetails object of the user who requests permission * @param requestedAuthGroups The authorization groups of the target object * @return boolean value - true if permission is granted, false otherwise. */ private boolean hasPermissionToDeploy(final CloudifyAuthorizationDetails authDetails, final Collection<String> requestedAuthGroups) { //if authGroups were not defined for this object - only cloud admins can see it if (requestedAuthGroups.isEmpty()) { return (isCloudAdmin(authDetails.getRoles())); } //if the current user has at any of the requested auth groups - deploy is permitted. return hasAnyAuthGroup(authDetails, requestedAuthGroups); //return hasAllAuthGroups(requestedAuthGroups); } private boolean hasAllAuthGroups(final Collection<String> requestedAuthGroups) { boolean isPermitted = false; Collection<String> userAuthGroups = getUserAuthGroups(); if (userAuthGroups.containsAll(requestedAuthGroups)) { isPermitted = true; } return isPermitted; } private boolean hasAnyAuthGroup(final CloudifyAuthorizationDetails authDetails, final Collection<String> requestedAuthGroups) { boolean isPermitted = false; Collection<String> userAuthGroups = authDetails.getAuthGroups(); for (String requestedAuthGroup : requestedAuthGroups) { /*if (userAuthGroups.contains(requestedAuthGroup)) { isPermitted = true; break; }*/ for (String userAuthGroup : userAuthGroups) { if (requestedAuthGroup.equalsIgnoreCase(userAuthGroup)) { isPermitted = true; break; } } } return isPermitted; } /** * Returns the names of the roles (authorities) the user is granted. * @param authentication The authentication object of the current user * @return A Collection of roles (authorities) the user is granted. */ private Collection<String> getUserRoles(final Authentication authentication) { Set<String> userRoles = new HashSet<String>(); if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("Anonymous user is not supported"); } if (!(authentication instanceof UsernamePasswordAuthenticationToken)) { throw new AccessDeniedException("Authentication object type not supported. " + "Verify your Spring configuration is valid."); } for (GrantedAuthority authority : authentication.getAuthorities()) { userRoles.add(authority.getAuthority()); } return userRoles; } /** * Returns the names of the roles (authorities) the user is granted. * @return A Collection of roles (authorities) the user is granted. */ private Collection<String> getUserRoles() { return getUserRoles(SecurityContextHolder.getContext().getAuthentication()); } /** * Returns the names of the roles (authorities) the user is granted. * @return A String array of roles (authorities) names */ public String getUserRolesString() { Collection<String> userRoles = getUserRoles(); return collectionToDelimitedString(userRoles, ","); } /** * Returns the names of the roles (authorities) the user is granted. * @param authentication The authentication object of the current user * @return A String array of roles (authorities) names */ public String getUserRolesString(final Authentication authentication) { Collection<String> userRoles = getUserRoles(authentication); return collectionToDelimitedString(userRoles, ","); } /** * Returns the names of the authorization groups the user belongs to. * @param authentication The authentication object of the current user * @return A String array of authorization groups names */ public String getUserAuthGroupsString(final Authentication authentication) { Collection<String> userAuthGroups = getUserAuthGroups(authentication); return collectionToDelimitedString(userAuthGroups, ","); } /** * Returns the names of the authorization groups the user belongs to. * @return A String array of authorization groups names */ public String getUserAuthGroupsString() { Collection<String> userAuthGroups = getUserAuthGroups(); return collectionToDelimitedString(userAuthGroups, ","); } /** * Returns the names of the authorization groups the user belongs to. * @return A Collection of authorization groups the user belongs to. */ private Collection<String> getUserAuthGroups() { return getUserAuthGroups(SecurityContextHolder.getContext().getAuthentication()); } /** * Returns the names of the authorization groups the user belongs to. * @param authentication The authentication object of the current user * @return A Collection of authorization groups names */ private Collection<String> getUserAuthGroups(final Authentication authentication) { Collection<String> userAuthGroups; if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("Anonymous user is not supported"); } if (authentication instanceof CustomAuthenticationToken) { userAuthGroups = ((CustomAuthenticationToken) authentication).getAuthGroups(); } else { //auth groups don't exist in this configuration, so use roles as authGroups. userAuthGroups = getUserRoles(); } return userAuthGroups; } private boolean isCloudAdmin(final Collection<String> roles) { boolean hasAdminRole = false; for (String role : roles) { if (ROLE_CLOUDADMIN.equalsIgnoreCase(role)) { hasAdminRole = true; break; } } return hasAdminRole; } private boolean isLocalCloud() { String isLocalCloudStr = System.getenv(CloudifyConstants.GIGASPACES_CLOUD_MACHINE_ID); return LOCALCLOUD.equalsIgnoreCase(isLocalCloudStr); } /** * Splits the string by the specified delimiter and trims the resulting tokens. * @param stringOfTokens The string to split * @param delimiter The delimiter to split by * @return A Collection of trimmed String tokens */ private static Collection<String> splitAndTrimString(final String stringOfTokens, final String delimiter) { Collection<String> values = new HashSet<String>(); StringTokenizer tokenizer = new StringTokenizer(stringOfTokens, delimiter); while (tokenizer.hasMoreTokens()) { values.add(tokenizer.nextToken().trim()); } return values; } private static String collectionToDelimitedString(final Collection<String> collection, final String delimiter) { String delimitedString; StringBuilder builder = new StringBuilder(); for (String item : collection) { builder.append(item); builder.append(delimiter); } delimitedString = builder.toString(); if (delimitedString.endsWith(delimiter)) { delimitedString = delimitedString.substring(0, delimitedString.length() - delimiter.length()); } return delimitedString; } }