/* * Copyright (C) 2014-2015 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * * Akvo FLOW is free software: you can redistribute it and modify it under the terms of * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, * either version 3 of the License or any later version. * * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License included below for more details. * * The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>. */ package org.waterforpeople.mapping.app.web.rest.security; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import org.akvo.flow.domain.RootFolder; import org.akvo.flow.domain.SecuredObject; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.JsonToken; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; import org.springframework.security.web.FilterInvocation; import org.waterforpeople.mapping.dao.SurveyInstanceDAO; import com.gallatinsystems.common.Constants; import com.gallatinsystems.survey.dao.SurveyDAO; import com.gallatinsystems.survey.dao.SurveyGroupDAO; import com.gallatinsystems.user.dao.UserAuthorizationDAO; import com.gallatinsystems.user.dao.UserRoleDao; import com.gallatinsystems.user.domain.Permission; import com.gallatinsystems.user.domain.UserAuthorization; import com.gallatinsystems.user.domain.UserRole; public class RequestUriVoter implements AccessDecisionVoter<FilterInvocation> { private static final Logger log = Logger.getLogger(RequestUriVoter.class.getName()); private static String PROJECT_FOLDER_URI_PREFIX = Permission.PROJECT_FOLDER_CREATE .getUriPrefix(); private static String FORM_URI_PREFIX = Permission.FORM_CREATE.getUriPrefix(); private static String SURVEY_RESPONSE_URI_PREFIX = Permission.DATA_DELETE.getUriPrefix(); private static String URI_SUFFIX = "/(\\d*)"; private static final Pattern URI_PATTERN = Pattern.compile("(" + PROJECT_FOLDER_URI_PREFIX + "|" + FORM_URI_PREFIX + "|" + SURVEY_RESPONSE_URI_PREFIX + ")(" + URI_SUFFIX + ")?"); private static final Pattern OBJECT_ID_PATTERN = Pattern.compile("(" + PROJECT_FOLDER_URI_PREFIX + "|" + FORM_URI_PREFIX + "|" + SURVEY_RESPONSE_URI_PREFIX + ")(" + URI_SUFFIX + ")"); private static final int PREFIX_GROUP = 1; private static final int OBJECT_ID_GROUP = 3; @Inject private UserRoleDao userRoleDao; @Inject private UserAuthorizationDAO userAuthorizationDao; @Inject private SurveyGroupDAO surveyGroupDao; @Inject private SurveyDAO surveyDao; @Inject private SurveyInstanceDAO surveyInstanceDao; @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return clazz.isAssignableFrom(FilterInvocation.class); } @Override public int vote(Authentication authentication, FilterInvocation securedObject, Collection<ConfigAttribute> attributes) { // abstain from voting if (abstainVote(authentication, securedObject)) { return ACCESS_ABSTAIN; } // on first login we need to abstain in order to set the user credentials for the // authentication object if (authentication.getCredentials() == null) { // immediately deny access if unable to identify user throw new AccessDeniedException( "Access is Denied. Unable to identify user"); } String requestUri = securedObject.getRequestUrl(); if (requestUri.startsWith(PROJECT_FOLDER_URI_PREFIX) || requestUri.startsWith(FORM_URI_PREFIX)) { return voteFolderSurveyUri(authentication, securedObject); } else if (requestUri.startsWith(SURVEY_RESPONSE_URI_PREFIX)) { return voteSurveyResponseUri(authentication, securedObject); } // catchall access denied return ACCESS_DENIED; } /** * Checks the secured object passed in via the request and determines whether the * RequestUriVoter will abstain from voting for an access decision * * @param securedObject * @return */ private boolean abstainVote(Authentication authentication, FilterInvocation securedObject) { if (!URI_PATTERN.matcher(securedObject.getRequestUrl()).find()) { // request URL does not match the URI patterns we consider for voting return true; } else if (authentication.getAuthorities().contains(AppRole.SUPER_ADMIN)) { // requester is a super admin user no need to control access or return true; } else if ("GET".equals(securedObject.getHttpRequest().getMethod()) && parseObjectId(securedObject.getRequestUrl()) == null) { // all GET requests for a set of entities are filtered // via the BaseDAO.filterByUserAuthorizationObjectId() return true; } else { return false; } } /** * Vote access decision for requests to the projects/folders/surveys URIs * * @param authentication * @param securedObject * @return */ private int voteFolderSurveyUri(Authentication authentication, FilterInvocation securedObject) { HttpServletRequest request = securedObject.getHttpRequest(); String httpMethod = request.getMethod(); String requestUri = securedObject.getRequestUrl(); List<Long> ancestorIds = new ArrayList<Long>(); Long objectId = null; if ("GET".equals(httpMethod) || "PUT".equals(httpMethod) || "DELETE".equals(httpMethod)) { objectId = parseObjectId(requestUri); ancestorIds.addAll(retrieveAncestorIdsFromDataStore(parseRequestPrefix(requestUri), objectId)); } else if ("POST".equals(httpMethod)) { objectId = parsePayload(request); // POST requests always use FOLDER_URI prefix since we always create a folder or form // within another folder or survey respectively ancestorIds.addAll(retrieveAncestorIdsFromDataStore(PROJECT_FOLDER_URI_PREFIX, objectId)); } // Also check for scenario where the user/role combo has been coupled with the object and // not one of its ancestors. This is only valid for folders/surveys. boolean includeSecuredObjectId = objectId != null; if (includeSecuredObjectId) { ancestorIds.add(objectId); } return checkUserAuthorization(authentication, securedObject, ancestorIds); } /** * Retrieve the object id of the parent object associated with the payload. * * @param httpRequest * @return */ private Long parsePayload(HttpServletRequest httpRequest) { if (httpRequest.getContentType() == null || !httpRequest.getContentType().startsWith("application/json") || httpRequest.getContentLength() == 0) { return null; // if not JSON payload, conversion exception will be thrown later } boolean isValidPayload = false; String idString = null; try { JsonFactory f = new JsonFactory(); JsonParser parser = f.createJsonParser(httpRequest.getInputStream()); boolean isSurvey = false; boolean isForm = false; while (parser.nextToken() != JsonToken.END_OBJECT) { String field = parser.getCurrentName(); if (null == field) { continue; } else if ("survey_group".equals(field)) { isSurvey = true; } else if ("survey".equals(field)) { isForm = true; } else if ("parentId".equals(field)) { parser.nextToken(); idString = parser.getText(); isValidPayload = isSurvey && idString != null; } else if ("surveyGroupId".equals(field)) { parser.nextToken(); idString = parser.getText(); isValidPayload = isForm && idString != null; } } } catch (Exception e) { log.severe(e.getMessage()); return null; } if (!isValidPayload) { // missing id parameter or wrong combination of survey->parentId or form->surveyGroupId return null; } Long objectId = null; try { objectId = Long.parseLong(idString); } catch (NumberFormatException e) { log.warning("Unable to identify the requested object id: " + idString); } return objectId; } /** * Check the authorization of a user based on the ids of object being accessed or one of its * ancestors. * * @param authentication * @param resourcePath * @return */ private int checkUserAuthorization(Authentication authentication, FilterInvocation securedObject, List<Long> objectIds) { Long userId = (Long) authentication.getCredentials(); if (objectIds == null || objectIds.isEmpty()) { // no path found throw new AccessDeniedException( "Access is Denied. Unable to identify object(s)"); } // retrieve user authorizations containing resource paths that make up this one List<UserAuthorization> authorizations = userAuthorizationDao.listByObjectIds(userId, objectIds); if (authorizations.isEmpty()) { throw new AccessDeniedException("Access is Denied. Insufficient permissions"); } List<Long> authorizedRoleIds = new ArrayList<Long>(); for (UserAuthorization auth : authorizations) { authorizedRoleIds.add(auth.getRoleId()); } List<UserRole> authorizedRoles = userRoleDao.listByKeys(authorizedRoleIds .toArray(new Long[0])); Permission permission = Permission.lookup(securedObject.getHttpRequest().getMethod(), securedObject.getRequestUrl()); for (UserRole role : authorizedRoles) { if (role.getPermissions().contains(permission)) { return ACCESS_GRANTED; } } throw new AccessDeniedException("Access is Denied. Insufficient permissions"); } /** * Vote access decision for requests to survey response URIs * * @param authentication * @param securedObject * @return */ private int voteSurveyResponseUri(Authentication authentication, FilterInvocation securedObject) { String httpMethod = securedObject.getHttpRequest().getMethod(); String requestUri = securedObject.getRequestUrl(); List<Long> ancestorIds = new ArrayList<Long>(); if ("GET".equals(httpMethod) || "DELETE".equals(httpMethod)) { ancestorIds = retrieveAncestorIdsFromDataStore(parseRequestPrefix(requestUri), parseObjectId(requestUri)); } else if ("POST".equals(httpMethod) || "PUT".equals(httpMethod)) { // no post or put for survey instances is allowed via rest API at the moment String message = "This operation is not supported operation at the moment."; log.warning("POST/PUT survey responses :" + message); throw new AccessDeniedException( "Access is Denied. " + message); } return checkUserAuthorization(authentication, securedObject, ancestorIds); } /** * Parse request prefix from the requestUri * * @param requestUri * @return */ private String parseRequestPrefix(String requestUri) { Matcher requestUriMatcher = URI_PATTERN.matcher(requestUri); if (requestUriMatcher.find()) { return requestUriMatcher.group(PREFIX_GROUP); } return null; } /** * Identify requested object id from requestUri * * @param requestUri * @return */ private Long parseObjectId(String requestUri) { Matcher objectIdMatcher = OBJECT_ID_PATTERN.matcher(requestUri); if (objectIdMatcher.find()) { String objectIdStr = objectIdMatcher.group(OBJECT_ID_GROUP); return Long.parseLong(objectIdStr); } return null; } /** * Retrieve the ancestorIds for a secured object from the datastore * * @param requestPrefix * @param objectId * @return */ private List<Long> retrieveAncestorIdsFromDataStore(String requestPrefix, Long objectId) { if (requestPrefix == null || objectId == null) { log.warning("Failed to identify request parameters requestPrefix=" + requestPrefix + "; object=" + objectId); return Collections.emptyList(); } List<Long> ancestorIds = new ArrayList<Long>(); SecuredObject obj = null; if (Constants.ROOT_FOLDER_ID.equals(objectId)) { obj = new RootFolder(); } else if (SURVEY_RESPONSE_URI_PREFIX.equals(requestPrefix)) { obj = surveyInstanceDao.getByKey(objectId); } else if (FORM_URI_PREFIX.equals(requestPrefix)) { obj = surveyDao.getByKey(objectId); } else if (PROJECT_FOLDER_URI_PREFIX.equals(requestPrefix)) { obj = surveyGroupDao.getByKey(objectId); } if (obj != null) { ancestorIds.addAll(obj.listAncestorIds()); } return ancestorIds; } }