Implemented four eyes principle.
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index 496bd2d..beea82f 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -329,6 +329,7 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = TaskExecutionBySameUserAsCaseCreation.class)
void markTaskExecution(@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("taskidentifier") final String taskIdentifier,
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskExecutionBySameUserAsCaseCreation.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskExecutionBySameUserAsCaseCreation.java
new file mode 100644
index 0000000..f4de44f
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskExecutionBySameUserAsCaseCreation.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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 io.mifos.portfolio.api.v1.client;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TaskExecutionBySameUserAsCaseCreation extends RuntimeException {
+}
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index efca4f2..9e07c3b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -270,7 +270,7 @@
ret.setName("feep");
ret.setMandatory(false);
ret.setActions(new HashSet<>());
- ret.setFourEyes(true);
+ ret.setFourEyes(false);
return ret;
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
index 5f3d324..f5e446c 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
@@ -15,13 +15,16 @@
*/
package io.mifos.portfolio;
+import io.mifos.core.api.context.AutoUserContext;
import io.mifos.core.api.util.NotFoundException;
import io.mifos.core.test.domain.TimeStampChecker;
+import io.mifos.portfolio.api.v1.client.TaskExecutionBySameUserAsCaseCreation;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.Product;
import io.mifos.portfolio.api.v1.domain.TaskDefinition;
import io.mifos.portfolio.api.v1.domain.TaskInstance;
import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
import io.mifos.portfolio.api.v1.events.TaskInstanceEvent;
import org.junit.Assert;
import org.junit.Test;
@@ -93,6 +96,32 @@
Assert.assertTrue(allTasksForCase.contains(executedTaskInstance));
}
+ @Test
+ public void shouldMarkBasicTaskNotExecuted() throws InterruptedException {
+ final Product product = createProduct();
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+ enableProduct(product);
+
+ final Case customerCase = createCase(product.getIdentifier());
+
+ portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), true);
+ Assert.assertTrue(eventRecorder.wait(EventConstants.PUT_TASK_INSTANCE_EXECUTION, new TaskInstanceEvent(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier())));
+
+ final TaskInstance executedTaskInstance = portfolioManager.getTaskForCase(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier());
+ Assert.assertNotNull(executedTaskInstance.getExecutedBy());
+ Assert.assertNotNull(executedTaskInstance.getExecutedOn());
+
+ portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), false);
+ Assert.assertTrue(eventRecorder.wait(EventConstants.PUT_TASK_INSTANCE_EXECUTION, new TaskInstanceEvent(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier())));
+
+ final TaskInstance unExecutedTaskInstance = portfolioManager.getTaskForCase(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier());
+ Assert.assertNull(unExecutedTaskInstance.getExecutedOn());
+ Assert.assertNull(unExecutedTaskInstance.getExecutedBy());
+
+ final List<TaskInstance> tasksForCaseExcludingExecuted = portfolioManager.getAllTasksForCase(product.getIdentifier(), customerCase.getIdentifier(), false);
+ Assert.assertTrue(tasksForCaseExcludingExecuted.contains(unExecutedTaskInstance));
+ }
@Test(expected = NotFoundException.class)
public void shouldFailToMarkNonExistantTask() throws InterruptedException {
@@ -105,4 +134,40 @@
portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), "blubble", true);
}
+
+
+ @Test(expected = TaskExecutionBySameUserAsCaseCreation.class)
+ public void fourEyesCannotBeMarkedByCaseCreator() throws InterruptedException {
+ final Product product = createProduct();
+
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+ taskDefinition.setFourEyes(true);
+ portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
+ eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier()));
+
+ enableProduct(product);
+
+ final Case customerCase = createCase(product.getIdentifier());
+
+ portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), true);
+ }
+
+
+ @Test
+ public void fourEyesCanBeMarkedByDifferentUser() throws InterruptedException {
+ final Product product = createProduct();
+
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+ taskDefinition.setFourEyes(true);
+ portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
+ eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier()));
+
+ enableProduct(product);
+
+ final Case customerCase = createCase(product.getIdentifier());
+
+ try (final AutoUserContext ignored = this.tenantApplicationSecurityEnvironment.createAutoUserContext("fred")) {
+ portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), true);
+ }
+ }
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskInstanceCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskInstanceCommandHandler.java
index 3d28ae7..5e32df0 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskInstanceCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskInstanceCommandHandler.java
@@ -82,8 +82,14 @@
.orElseThrow(() -> ServiceException.notFound("Task instance ''{0}.{1}.{2}'' not found.",
productIdentifier, caseIdentifier, taskIdentifier));
- taskInstanceEntity.setExecutedOn(LocalDateTime.now(Clock.systemUTC()));
- taskInstanceEntity.setExecutedBy(UserContextHolder.checkedGetUser());
+ if (executed) {
+ taskInstanceEntity.setExecutedOn(LocalDateTime.now(Clock.systemUTC()));
+ taskInstanceEntity.setExecutedBy(UserContextHolder.checkedGetUser());
+ }
+ else {
+ taskInstanceEntity.setExecutedOn(null);
+ taskInstanceEntity.setExecutedBy(null);
+ }
taskInstanceRepository.save(taskInstanceEntity);
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
index 2180b7e..001d60a 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
@@ -17,9 +17,12 @@
import io.mifos.anubis.annotation.AcceptedTokenType;
import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.command.gateway.CommandGateway;
import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.PermittableGroupIds;
+import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.TaskDefinition;
import io.mifos.portfolio.api.v1.domain.TaskInstance;
import io.mifos.portfolio.service.internal.command.ChangeTaskInstanceCommand;
import io.mifos.portfolio.service.internal.command.ExecuteTaskInstanceCommand;
@@ -71,7 +74,7 @@
@PathVariable("caseidentifier") final String caseIdentifier,
@RequestParam(value = "includeExecuted", required = false) final Boolean includeExecuted)
{
- checkCaseExists(productIdentifier, caseIdentifier);
+ checkedGetCase(productIdentifier, caseIdentifier);
return taskInstanceService.findAllEntities(productIdentifier, caseIdentifier, includeExecuted);
}
@@ -85,10 +88,10 @@
)
public @ResponseBody
TaskInstance getTaskForCase(@PathVariable("productidentifier") final String productIdentifier,
- @PathVariable("caseidentifier") final String caseIdentifier,
- @PathVariable("taskidentifier") final String taskIdentifier)
+ @PathVariable("caseidentifier") final String caseIdentifier,
+ @PathVariable("taskidentifier") final String taskIdentifier)
{
- checkCaseExists(productIdentifier, caseIdentifier);
+ checkedGetCase(productIdentifier, caseIdentifier);
return taskInstanceService.findByIdentifier(productIdentifier, caseIdentifier, taskIdentifier).orElseThrow(
() -> ServiceException.notFound("No task instance ''{0}.{1}.{2}'' found.", productIdentifier, caseIdentifier, taskIdentifier));
@@ -107,9 +110,9 @@
@PathVariable("taskidentifier") final String taskIdentifier,
@RequestBody @Valid final TaskInstance instance)
{
- checkCaseExists(productIdentifier, caseIdentifier);
+ checkedGetCase(productIdentifier, caseIdentifier);
- checkTaskDefinitionExists(productIdentifier, taskIdentifier);
+ checkedGetTaskDefinition(productIdentifier, taskIdentifier);
if (!taskIdentifier.equals(instance.getTaskIdentifier()))
throw ServiceException.badRequest("Instance identifiers may not be changed.");
@@ -132,21 +135,26 @@
@PathVariable("taskidentifier") final String taskIdentifier,
@RequestBody @Valid final Boolean executed)
{
- checkTaskDefinitionExists(productIdentifier, taskIdentifier);
+ final TaskDefinition taskDefinition = checkedGetTaskDefinition(productIdentifier, taskIdentifier);
+ if (taskDefinition.getFourEyes()) {
+ final Case customerCase = checkedGetCase(productIdentifier, caseIdentifier);
+ if (UserContextHolder.checkedGetUser().equals(customerCase.getCreatedBy()))
+ throw ServiceException.conflict("Signing user must be different than case creator.");
+ }
commandGateway.process(new ExecuteTaskInstanceCommand(productIdentifier, caseIdentifier, taskIdentifier, executed));
return ResponseEntity.accepted().build();
}
- private void checkTaskDefinitionExists(final String productIdentifier,
- final String taskDefinitionIdentifier) throws ServiceException {
- taskDefinitionService.findByIdentifier(productIdentifier, taskDefinitionIdentifier)
+ private TaskDefinition checkedGetTaskDefinition(final String productIdentifier,
+ final String taskDefinitionIdentifier) throws ServiceException {
+ return taskDefinitionService.findByIdentifier(productIdentifier, taskDefinitionIdentifier)
.orElseThrow(() -> ServiceException.notFound("No task with the identifier ''{0}.{1}'' exists.", productIdentifier, taskDefinitionIdentifier));
}
- private void checkCaseExists(final String productIdentifier, final String caseIdentifier) throws ServiceException {
- caseService.findByIdentifier(productIdentifier, caseIdentifier)
- .orElseThrow(() -> ServiceException.notFound("Case ''{0}.{2}'' does not exist.", productIdentifier, caseIdentifier));
+ private Case checkedGetCase(final String productIdentifier, final String caseIdentifier) throws ServiceException {
+ return caseService.findByIdentifier(productIdentifier, caseIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Case ''{0}.{1}'' does not exist.", productIdentifier, caseIdentifier));
}
}