/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you 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.wso2.carbon.bpmn.rest.service.runtime;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.wso2.carbon.bpmn.core.BPMNConstants;
import org.wso2.carbon.bpmn.core.mgt.model.SubstitutesDataModel;
import org.wso2.carbon.bpmn.people.substitution.SubstitutionDataHolder;
import org.wso2.carbon.bpmn.people.substitution.SubstitutionQueryProperties;
import org.wso2.carbon.bpmn.people.substitution.UserSubstitutionUtils;
import org.wso2.carbon.bpmn.rest.common.exception.BPMNForbiddenException;
import org.wso2.carbon.bpmn.rest.common.utils.BPMNOSGIService;
import org.wso2.carbon.bpmn.rest.model.common.BooleanResponse;
import org.wso2.carbon.bpmn.rest.model.runtime.*;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
/**
* REST endpoints related to automatic task reassignment and substitution.
*/
@Path("/substitutes")
public class UserSubstitutionService {
private static final Log log = LogFactory.getLog(UserSubstitutionService.class);
@Context UriInfo uriInfo;
private static final String ASCENDING = "asc";
private static final String DESCENDING = "desc";
private static final String ADD_PERMISSION = "add";
private static final String DEFAULT_PAGINATION_START = "0";
private static final String DEFAULT_PAGINATION_SIZE = "10";
private static final String TRUE = "true";
private static final String FALSE = "false";
private static final boolean subsFeatureEnabled = SubstitutionDataHolder.getInstance().isSubstitutionFeatureEnabled();
protected static final HashMap<String, String> propertiesMap = new HashMap<>();
static {
propertiesMap.put("assignee", SubstitutionQueryProperties.USER);
propertiesMap.put("substitute", SubstitutionQueryProperties.SUBSTITUTE);
propertiesMap.put("enabled", SubstitutionQueryProperties.ENABLED);
propertiesMap.put("start", SubstitutionQueryProperties.SUBSTITUTION_START);
propertiesMap.put("end", SubstitutionQueryProperties.SUBSTITUTION_END);
propertiesMap.put("start", SubstitutionQueryProperties.START);
propertiesMap.put("size", SubstitutionQueryProperties.SIZE);
propertiesMap.put("order", SubstitutionQueryProperties.ORDER);
propertiesMap.put("sort", SubstitutionQueryProperties.SORT);
}
/**
* Add new addSubstituteInfo record.
* Following request body parameters are required,
* assignee : optional, logged in user is used if not provided
* substitute : required
* startTime : optional, current timestamp if not provided, the timestamp the substitution should start in ISO format
* endTime : optional, considered as forever if not provided, the timestamp the substitution should end in ISO format
* @param request
* @return 201 created response with the resource location. 405 if substitution disabled
*/
@POST
@Path("/")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response substitute(SubstitutionRequest request) {
try {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
String assignee = getRequestedAssignee(request.getAssignee());
String substitute = validateAndGetSubstitute(request.getSubstitute(), assignee);
Date endTime = null;
Date startTime = new Date();
DateTime requestStartTime = null;
if (request.getStartTime() != null) {
requestStartTime = new DateTime(request.getStartTime());
startTime = new Date(requestStartTime.getMillis());
}
if (request.getEndTime() != null) {
endTime = validateEndTime(request.getEndTime(), requestStartTime);
}
if (!UserSubstitutionUtils.validateTasksList(request.getTaskList(), assignee)) {
throw new ActivitiIllegalArgumentException("Invalid task list provided, for substitution.");
}
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
//at this point, substitution is enabled by default
UserSubstitutionUtils
.handleNewSubstituteAddition(assignee, substitute, startTime, endTime, true, request.getTaskList(),
tenantId);
return Response.created(new URI("substitutes/" + assignee)).build();
} catch (UserStoreException e) {
throw new ActivitiException("Error accessing User Store", e);
} catch (URISyntaxException e) {
throw new ActivitiException("Response location URI creation header", e);
} catch (ActivitiIllegalArgumentException e) {
throw new ActivitiIllegalArgumentException(e.getMessage());
}
}
/**
* Update the substitute info of the given user in the request path. Use the same format used in POST method.
* @param user - user that need to update his substitute info
* @param request - substitute info that need to be updated
* @return
* @throws URISyntaxException
*/
@PUT
@Path("/{user}")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response updateSubstituteInfo(@PathParam("user") String user, SubstitutionRequest request) throws
URISyntaxException {
try {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
request.setAssignee(user);
String assignee = getRequestedAssignee(user);
String substitute = validateAndGetSubstitute(request.getSubstitute(), assignee);
Date endTime = null;
Date startTime = new Date();
DateTime requestStartTime = null;
if (request.getStartTime() != null) {
requestStartTime = new DateTime(request.getStartTime());
startTime = new Date(requestStartTime.getMillis());
}
if (request.getEndTime() != null) {
endTime = validateEndTime(request.getEndTime(), requestStartTime);
}
if (!UserSubstitutionUtils.validateTasksList(request.getTaskList(), assignee)) {
throw new ActivitiIllegalArgumentException("Invalid task list provided, for substitution.");
}
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
UserSubstitutionUtils
.handleUpdateSubstitute(assignee, substitute, startTime, endTime, true, request.getTaskList(), tenantId);
return Response.ok().build();
} catch (UserStoreException e) {
throw new ActivitiException("Error accessing User Store", e);
}
}
/**
* Change the substitute of the {user}. Use following request body format.
* {"substitute":"user"}
* @param user
* @param request
* @return
* @throws URISyntaxException
*/
@PUT
@Path("/{user}/substitute")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response changeSubstitute(@PathParam("user") String user, SubstituteRequest request)
throws URISyntaxException {
try {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
String assignee = getRequestedAssignee(user);
String substitute = validateAndGetSubstitute(request.getSubstitute(), assignee);
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
UserSubstitutionUtils.handleChangeSubstitute(assignee, substitute, tenantId);
} catch (UserStoreException e) {
throw new ActivitiException("Error accessing User Store", e);
}
return Response.ok().build();
}
/**
* Return the substitute info for the given user in path parameter
* @param user
* @return SubstituteInfoResponse
* @throws URISyntaxException
*/
@GET
@Path("/{user}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getSubstitute(@PathParam("user") String user) throws UserStoreException {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
user = getTenantAwareUser(user);
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
String loggedInUser = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername();
if (!loggedInUser.equals(user) && !isUserAuthorizedForSubstitute(loggedInUser)) {
throw new BPMNForbiddenException("Not allowed to view others substitution details. No sufficient permission");
}
SubstitutesDataModel model = UserSubstitutionUtils.getSubstituteOfUser(user, tenantId);
if (model != null) {
SubstituteInfoResponse response = new SubstituteInfoResponse();
response.setSubstitute(model.getSubstitute());
response.setAssignee(model.getUser());
response.setEnabled(model.isEnabled());
response.setStartTime(model.getSubstitutionStart());
response.setEndTime(model.getSubstitutionEnd());
return Response.ok(response).build();
} else {
return Response.status(404).build();
}
}
/**
* Check the logged in user has permission for viewing other substitutions.
* @return true if the permission sufficient
* @throws UserStoreException
*/
private boolean isUserAuthorizedForSubstitute(String username) throws UserStoreException {
UserRealm userRealm = BPMNOSGIService.getUserRealm();
//check with bpmn permission path
String[] permissionArray = userRealm.getAuthorizationManager()
.getAllowedUIResourcesForUser(username, BPMNConstants.BPMN_PERMISSION_PATH);
if (permissionArray != null && permissionArray.length > 0) {
if (permissionArray[0].equals(BPMNConstants.BPMN_PERMISSION_PATH) || isPermissionExist(permissionArray,
BPMNConstants.SUBSTITUTION_PERMISSION_PATH)) {
return true;
}
}
//check for admin permission
String[] adminPermissionArray = userRealm.getAuthorizationManager()
.getAllowedUIResourcesForUser(username, BPMNConstants.ROOT_PERMISSION_PATH);
if (adminPermissionArray != null && adminPermissionArray.length > 0) {
if (adminPermissionArray[0].equals(BPMNConstants.ROOT_PERMISSION_PATH) || adminPermissionArray[0]
.equals(BPMNConstants.ADMIN_PERMISSION_PATH)) {
return true;
}
}
return false;
}
private boolean isPermissionExist(String[] permissionArray, String path) {
for (int i = 0; i < permissionArray.length; i++) {
if (permissionArray[i].contains(BPMNConstants.SUBSTITUTION_PERMISSION_PATH)) {
return true;
}
}
return false;
}
/**
* Query the substitution records based on substitute, assignee and enabled or disabled.
* Pagination parameters, start, size, sort, order are allowed.
* @return paginated list of substitution info records
*/
@GET
@Path("/")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response querySubstitutes() {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
Map<String, String> queryMap = new HashedMap();
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String value = uriInfo.getQueryParameters().getFirst(entry.getKey());
if (value != null) {
queryMap.put(entry.getValue(), value);
}
}
//validate the parameters
try {
//replace with tenant aware user names
String tenantAwareUser = getTenantAwareUser(queryMap
.get(SubstitutionQueryProperties.USER));
queryMap.put(SubstitutionQueryProperties.USER, tenantAwareUser);
String tenantAwareSub = getTenantAwareUser(queryMap.get(SubstitutionQueryProperties.SUBSTITUTE));
queryMap.put(SubstitutionQueryProperties.SUBSTITUTE, tenantAwareSub);
if (!isUserAuthorizedForSubstitute(PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername())) {
String loggedInUser = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername();
if (!((queryMap.get(SubstitutionQueryProperties.USER) != null && queryMap
.get(SubstitutionQueryProperties.USER).equals(loggedInUser)) || (
queryMap.get(SubstitutionQueryProperties.SUBSTITUTE) != null && queryMap
.get(SubstitutionQueryProperties.SUBSTITUTE).equals(loggedInUser)))) {
throw new BPMNForbiddenException("Not allowed to view others substitution details. No sufficient permission");
}
}
} catch (UserStoreException e) {
throw new ActivitiException("Error accessing User Store for input validations", e);
}
//validate pagination parameters
validatePaginationParams(queryMap);
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
List<SubstitutesDataModel> dataModelList = UserSubstitutionUtils.querySubstitutions(queryMap, tenantId);
int totalResultCount = UserSubstitutionUtils.getQueryResultCount(queryMap, tenantId);
SubstituteInfoCollectionResponse collectionResponse = new SubstituteInfoCollectionResponse();
collectionResponse.setTotal(totalResultCount);
List<SubstituteInfoResponse> responseList = new ArrayList<>();
for (SubstitutesDataModel subsData : dataModelList) {
SubstituteInfoResponse response = new SubstituteInfoResponse();
response.setEnabled(subsData.isEnabled());
response.setEndTime(subsData.getSubstitutionEnd());
response.setStartTime(subsData.getSubstitutionStart());
response.setSubstitute(subsData.getSubstitute());
response.setAssignee(subsData.getUser());
responseList.add(response);
}
collectionResponse.setSubstituteInfoList(responseList);
collectionResponse.setSize(responseList.size());
String sortType = getSortType(queryMap.get(SubstitutionQueryProperties.SORT));
collectionResponse.setSort(sortType);
collectionResponse.setStart(Integer.parseInt(queryMap.get(SubstitutionQueryProperties.START)));
collectionResponse.setOrder(queryMap.get(SubstitutionQueryProperties.ORDER));
return Response.ok(collectionResponse).build();
}
/**
* Change the status of a substitution record.
*
* @param user : assignee of the substitution
* @param request : format {"action" : "true/false"}
* @return HTTP 200 upon success
* @throws UserStoreException
*/
@POST
@Path("/{user}/disable")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response disableSubstitution(@PathParam("user") String user, RestActionRequest request)
throws UserStoreException {
if (!subsFeatureEnabled) {
return Response.status(405).build();
}
String assignee = getRequestedAssignee(user);
String action = request.getAction();
if (action != null) {
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId();
if (action.trim().equalsIgnoreCase(TRUE)) {
UserSubstitutionUtils.disableSubstitution(true, assignee, tenantId);
} else if (action.trim().equalsIgnoreCase(FALSE)) {
UserSubstitutionUtils.disableSubstitution(false, assignee, tenantId);
} else {
throw new ActivitiIllegalArgumentException("Invalid disable action : " + action + " specified");
}
} else {
throw new ActivitiIllegalArgumentException("No disable action specified");
}
return Response.ok().build();
}
/**
* Return true if the substitution feature is enabled.
* @return {"enabled":true/false}
*/
@GET
@Path("/configs/enabled")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response isSubstitutionFeatureEnabled() {
BooleanResponse response = new BooleanResponse();
if (subsFeatureEnabled) {
response.setEnabled(true);
} else {
response.setEnabled(false);
}
return Response.ok(response).build();
}
private String getSortType(String sortType) {
switch (sortType) {
case (SubstitutionQueryProperties.SUBSTITUTION_START):
return "startTime";
case (SubstitutionQueryProperties.SUBSTITUTION_END):
return "endTime";
case (SubstitutionQueryProperties.SUBSTITUTE):
return "substitute";
case (SubstitutionQueryProperties.USER):
return "assignee";
}
return "";
}
private void validatePaginationParams(Map<String, String> queryMap) {
String start = queryMap.get(SubstitutionQueryProperties.START);
String size = queryMap.get(SubstitutionQueryProperties.SIZE);
String sort = queryMap.get(SubstitutionQueryProperties.SORT);
String order = queryMap.get(SubstitutionQueryProperties.ORDER);
if (start != null) {
if (Integer.parseInt(start) < 0) {
throw new ActivitiIllegalArgumentException("Invalid argument for parameter 'start'");
}
} else {
start = DEFAULT_PAGINATION_START;
}
queryMap.put(SubstitutionQueryProperties.START, start);
if (size != null) {
if (Integer.valueOf(size) <= 0) {
throw new ActivitiIllegalArgumentException("Invalid argument for parameter 'size'");
}
} else {
size = DEFAULT_PAGINATION_SIZE;
}
queryMap.put(SubstitutionQueryProperties.SIZE, size);
if (sort != null) {
switch (sort) {
case ("startTime"):
sort = SubstitutionQueryProperties.SUBSTITUTION_START;
break;
case ("endTime"):
sort = SubstitutionQueryProperties.SUBSTITUTION_END;
break;
case ("substitute"):
sort = SubstitutionQueryProperties.SUBSTITUTE;
break;
case ("assignee"):
sort = SubstitutionQueryProperties.USER;
break;
default:
throw new ActivitiIllegalArgumentException("Invalid argument for parameter 'sort'");
}
} else {
sort = SubstitutionQueryProperties.SUBSTITUTION_START;
}
queryMap.put(SubstitutionQueryProperties.SORT, sort);
if (order != null) {
if (!ASCENDING.equalsIgnoreCase(order) && !DESCENDING.equalsIgnoreCase(order)) {
throw new ActivitiIllegalArgumentException("Invalid argument for parameter 'order'");
}
} else {
order = "asc";
}
queryMap.put(SubstitutionQueryProperties.ORDER, order);
}
/**
* Return end time if valid
* @param endTime - should be non null
* @return Date
*/
private Date validateEndTime(String endTime, DateTime startTime) {
DateTime requestEndTime = new DateTime(endTime);
if (requestEndTime.isBeforeNow()) {
throw new ActivitiIllegalArgumentException("End time should be in future");
}
if (startTime != null) {
if (requestEndTime.isBefore(startTime.getMillis())) {
throw new ActivitiIllegalArgumentException("Invalid Start and End time combination");
}
}
return new Date(requestEndTime.getMillis());
}
/**
* Validate and get the assignee for a substitute request
* @param user
* @return actual assignee of the substitute request
* @throws UserStoreException
*/
private String getRequestedAssignee(final String user) throws UserStoreException {
String loggedInUser = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername();
UserRealm userRealm = BPMNOSGIService.getUserRealm();
String assignee = getTenantAwareUser(user);
//validate the assignee
if (assignee != null && !assignee.trim().isEmpty() && !assignee.equals(loggedInUser)) { //setting another users
boolean isAuthorized = isUserAuthorizedForSubstitute(loggedInUser);
if (!isAuthorized) {
throw new BPMNForbiddenException("Action requires BPMN substitution permission");
}
if (!userRealm.getUserStoreManager().isExistingUser(assignee)) {
throw new ActivitiIllegalArgumentException("Non existing user for argument assignee : " + assignee);
}
} else { //assignee is the logged in user
assignee = loggedInUser;
}
return assignee;
}
/**
* validate requested substitute user
* @param substitute
* @param assignee
* @return substitute name if valid
* @throws UserStoreException
*/
private String validateAndGetSubstitute(String substitute, String assignee) throws UserStoreException {
//validate substitute
UserRealm userRealm = BPMNOSGIService.getUserRealm();
if (substitute == null || substitute.trim().isEmpty()) {
throw new ActivitiIllegalArgumentException("The substitute must be specified");
} else {
substitute = getTenantAwareUser(substitute);
}
if (assignee.equalsIgnoreCase(substitute)) {
throw new ActivitiIllegalArgumentException("Substitute and assignee should be different users");
} else if (!userRealm.getUserStoreManager().isExistingUser(substitute.trim())) {
throw new ActivitiIllegalArgumentException("Cannot substitute a non existing user: " + substitute);
}
return substitute;
}
private String getTenantAwareUser(String user) {
String assignee = null;
if (user != null && !user.trim().isEmpty()) {
String currentDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(true);
String userDomain = MultitenantUtils.getTenantDomain(user);
if (!userDomain.equals(currentDomain)) {
throw new BPMNForbiddenException("Forbidden operation, tenancy violation.");
}
assignee = MultitenantUtils.getTenantAwareUsername(user);
}
return assignee;
}
}