You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by dj...@apache.org on 2026/07/06 10:08:03 UTC
(incubator-kie-kogito-runtimes) branch main updated: [incubator-kie-issues#2358] Transition from Created-Obsolete, Error and Exited for WS-HT User tasks (#4329)
This is an automated email from the ASF dual-hosted git repository.
josedee pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-kogito-runtimes.git
The following commit(s) were added to refs/heads/main by this push:
new 3d8187f11c [incubator-kie-issues#2358] Transition from Created-Obsolete, Error and Exited for WS-HT User tasks (#4329)
3d8187f11c is described below
commit 3d8187f11c597d839147f8220abf7439fb72b13a
Author: Deepak Joseph <15...@users.noreply.github.com>
AuthorDate: Mon Jul 6 15:37:59 2026 +0530
[incubator-kie-issues#2358] Transition from Created-Obsolete, Error and Exited for WS-HT User tasks (#4329)
* Skip allowed from Created-Obsolete for WS-HT Usertasks
* update
* Created-Error,Exited added for BA
* Fixed exception handler for UserTaskTransitionException
---
.../impl/lifecycle/WsHumanTaskLifeCycle.java | 69 +++++++-----
.../UserTaskTransitionExceptionMapper.java | 6 +-
.../usertask/jpa/it/WsHumanTaskLifeCycleIT.java | 120 +++++++++++++++++++++
.../exceptions/springboot/ExceptionsHandler.java | 6 ++
.../usertask/jpa/it/WsHumanTaskLifeCycleIT.java | 120 +++++++++++++++++++++
5 files changed, 289 insertions(+), 32 deletions(-)
diff --git a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/WsHumanTaskLifeCycle.java b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/WsHumanTaskLifeCycle.java
index ce2c2625bc..0a19d611b4 100644
--- a/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/WsHumanTaskLifeCycle.java
+++ b/jbpm/jbpm-usertask/src/main/java/org/kie/kogito/usertask/impl/lifecycle/WsHumanTaskLifeCycle.java
@@ -96,6 +96,9 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
private final UserTaskTransition T_CREATED_READY_ACTIVATE = new DefaultUserTransition(ACTIVATE, CREATED, READY, this::activate);
private final UserTaskTransition T_CREATED_READY_NOMINATE = new DefaultUserTransition(NOMINATE, CREATED, READY, this::nominate);
+ private final UserTaskTransition T_CREATED_OBSOLETE = new DefaultUserTransition(SKIP, CREATED, OBSOLETE, this::skip);
+ private final UserTaskTransition T_CREATED_EXITED = new DefaultUserTransition(EXIT, CREATED, EXITED, this::exit);
+ private final UserTaskTransition T_CREATED_ERROR = new DefaultUserTransition(FAULT, CREATED, ERROR, this::fault);
private final UserTaskTransition T_READY_READY_FORWARD = new DefaultUserTransition(FORWARD, READY, READY, this::forward);
private final UserTaskTransition T_READY_RESERVED_CLAIM = new DefaultUserTransition(CLAIM, READY, RESERVED, this::claim);
@@ -132,11 +135,16 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
private final UserTaskTransition T_SUSPENDED_EXITED = new DefaultUserTransition(EXIT, SUSPENDED, EXITED, this::resumeAndExit);
private List<UserTaskTransition> transitions;
+ private Set<UserTaskTransition> engineOnlyTransitions;
+ private Set<UserTaskTransition> adminOnlyTransitions;
public WsHumanTaskLifeCycle() {
transitions = List.of(
T_CREATED_READY_ACTIVATE,
T_CREATED_READY_NOMINATE,
+ T_CREATED_OBSOLETE,
+ T_CREATED_EXITED,
+ T_CREATED_ERROR,
T_READY_READY_FORWARD,
T_READY_RESERVED_CLAIM,
T_READY_RESERVED_DELEGATE,
@@ -167,17 +175,23 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
T_SUSPENDED_RESERVED,
T_SUSPENDED_INPROGRESS,
T_SUSPENDED_EXITED);
+ engineOnlyTransitions = Set.of(T_CREATED_READY_ACTIVATE, T_SUSPENDED_EXITED);
+ adminOnlyTransitions = Set.of(T_CREATED_READY_NOMINATE, T_CREATED_OBSOLETE, T_CREATED_EXITED, T_CREATED_ERROR);
}
@Override
public List<UserTaskTransition> allowedTransitions(UserTaskInstance userTaskInstance, IdentityProvider identity) {
checkPermission(userTaskInstance, identity);
+ boolean isAdmin = isAdmin(userTaskInstance, identity);
+ String previousStatus = Optional.ofNullable(userTaskInstance.getMetadata().get(PREVIOUS_STATUS))
+ .map(Object::toString)
+ .orElse(null);
return transitions.stream()
- .filter(t -> t.source().equals(userTaskInstance.getStatus())
- && (!t.id().equals(SKIP) || "true".equals(userTaskInstance.getMetadata().get(SKIPPABLE)))
- && (!t.id().equals(RESUME) || t.target().getName().equals(userTaskInstance.getMetadata().get(PREVIOUS_STATUS).toString()))
- && t != T_SUSPENDED_EXITED
- && t != T_CREATED_READY_ACTIVATE)
+ .filter(t -> t.source().equals(userTaskInstance.getStatus()))
+ .filter(t -> !engineOnlyTransitions.contains(t))
+ .filter(t -> !adminOnlyTransitions.contains(t) || isAdmin)
+ .filter(t -> !t.id().equals(SKIP) || "true".equals(userTaskInstance.getMetadata().get(SKIPPABLE)))
+ .filter(t -> !t.id().equals(RESUME) || t.target().getName().equals(previousStatus))
.toList();
}
@@ -247,7 +261,7 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
}
public Optional<UserTaskTransitionToken> nominate(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) {
- if (!userTaskInstance.getAdminUsers().contains(identityProvider.getName()) && Collections.disjoint(userTaskInstance.getAdminGroups(), identityProvider.getRoles())) {
+ if (!isAdmin(userTaskInstance, identityProvider)) {
throw new UserTaskTransitionException("User is not allowed to nominate");
}
@@ -258,7 +272,7 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
throw new UserTaskTransitionException("Illegal format for nominated users");
}
} else {
- throw new UserTaskTransitionException("No potential users specified");
+ throw new UserTaskTransitionException("Nominated user(s) are not specified");
}
return activate(userTaskInstance, token, identityProvider);
@@ -356,6 +370,9 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
}
public Optional<UserTaskTransitionToken> skip(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) {
+ if (CREATED.equals(userTaskInstance.getStatus()) && !isAdmin(userTaskInstance, identityProvider)) {
+ throw new UserTaskTransitionException("User is not allowed to skip");
+ }
if (!Boolean.parseBoolean((String) userTaskInstance.getMetadata().get(SKIPPABLE))) {
throw new UserTaskTransitionException("Usertask cannot be skipped");
}
@@ -491,25 +508,25 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
return assignmentStrategy.computeAssignment(userTaskInstance, identityProvider).orElse(null);
}
+ private boolean isAdmin(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) {
+ return isAdmin(userTaskInstance, identityProvider.getName(), identityProvider.getRoles());
+ }
+
+ private boolean isAdmin(UserTaskInstance userTaskInstance, String user, Collection<String> roles) {
+ return userTaskInstance.getAdminUsers().contains(user)
+ || !Collections.disjoint(userTaskInstance.getAdminGroups(), roles);
+ }
+
private void checkPermission(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) {
this.checkPermission(userTaskInstance, identityProvider.getName(), identityProvider.getRoles());
}
private void checkPermission(UserTaskInstance userTaskInstance, String user, Collection<String> roles) {
-
if (WORKFLOW_ENGINE_USER.equals(user)) {
return;
}
- // first we check admins
- Set<String> adminUsers = userTaskInstance.getAdminUsers();
- if (adminUsers.contains(user)) {
- return;
- }
-
- Set<String> userAdminGroups = new HashSet<>(userTaskInstance.getAdminGroups());
- userAdminGroups.retainAll(roles);
- if (!userAdminGroups.isEmpty()) {
+ if (isAdmin(userTaskInstance, user, roles)) {
return;
}
@@ -517,27 +534,21 @@ public class WsHumanTaskLifeCycle implements UserTaskLifeCycle {
return;
}
- Set<String> excludedUsers = userTaskInstance.getExcludedUsers();
- if (excludedUsers != null && excludedUsers.contains(user)) {
+ if (userTaskInstance.getExcludedUsers().contains(user)) {
String message = String.format("User '%s' is not authorized to perform an operation on user task '%s'",
user, userTaskInstance.getId());
throw new UserTaskInstanceNotAuthorizedException(message);
}
- if (CREATED.equals(userTaskInstance.getStatus())
- || READY.equals(userTaskInstance.getStatus())
- || (SUSPENDED.equals(userTaskInstance.getStatus())
- && READY.getName().equals(userTaskInstance.getMetadata().get(PREVIOUS_STATUS)))) {
+ if (READY.equals(userTaskInstance.getStatus())
+ || (SUSPENDED.equals(userTaskInstance.getStatus()) && READY.getName().equals(userTaskInstance.getMetadata().get(PREVIOUS_STATUS)))) {
- Set<String> users = new HashSet<>(userTaskInstance.getPotentialUsers());
- users.removeAll(userTaskInstance.getExcludedUsers());
- if (users.contains(user)) {
+ if (userTaskInstance.getPotentialUsers().contains(user)
+ && !userTaskInstance.getExcludedUsers().contains(user)) {
return;
}
- Set<String> userPotGroups = new HashSet<>(userTaskInstance.getPotentialGroups());
- userPotGroups.retainAll(roles);
- if (!userPotGroups.isEmpty()) {
+ if (!Collections.disjoint(userTaskInstance.getPotentialGroups(), roles)) {
return;
}
}
diff --git a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java
index 063439ead6..4ed2195290 100644
--- a/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java
+++ b/quarkus/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/UserTaskTransitionExceptionMapper.java
@@ -18,20 +18,20 @@
*/
package org.kie.kogito.resource.exceptions;
-import org.kie.kogito.usertask.UserTaskInstanceNotFoundException;
+import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
@Provider
-public class UserTaskTransitionExceptionMapper extends BaseExceptionMapper<UserTaskInstanceNotFoundException> {
+public class UserTaskTransitionExceptionMapper extends BaseExceptionMapper<UserTaskTransitionException> {
@Inject
ExceptionsHandler exceptionsHandler;
@Override
- public Response toResponse(UserTaskInstanceNotFoundException e) {
+ public Response toResponse(UserTaskTransitionException e) {
return exceptionsHandler.mapException(e);
}
}
diff --git a/quarkus/integration-tests/integration-tests-quarkus-wshumantasks/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java b/quarkus/integration-tests/integration-tests-quarkus-wshumantasks/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
index 9e2cc64605..d68b003ed1 100644
--- a/quarkus/integration-tests/integration-tests-quarkus-wshumantasks/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
+++ b/quarkus/integration-tests/integration-tests-quarkus-wshumantasks/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
@@ -676,6 +676,93 @@ public class WsHumanTaskLifeCycleIT {
isProcessCompleted(processId, pid);
}
+ @Test
+ public void testSkipFromCreatedTransition() {
+ var user = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(user, pid);
+ verifyTask(processId, pid, taskId, user, "Created", new String[] {});
+
+ skip(taskId, user);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotSkipFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ skipNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still skip it
+ skip(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testExitFromCreatedTransition() {
+ var admin = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ exit(taskId, admin);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotExitFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ exitNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still exit it
+ exit(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testFaultFromCreatedTransition() {
+ var admin = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ fault(taskId, admin);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotFaultFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ faultNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still fault it
+ fault(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
@Test
public void testSingleUserUserTaskLifeCycle() {
var user = "jdoe";
@@ -845,6 +932,39 @@ public class WsHumanTaskLifeCycleIT {
.body("status.terminate", equalTo("OBSOLETE"));
}
+ private void skipNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("skip"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
+ private void exitNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("exit"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
+ private void faultNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("fault"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
private void resume(String taskId, String user, String previousState) {
given()
.contentType(ContentType.JSON)
diff --git a/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java b/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java
index dd11be5d67..8e1a78cdb4 100644
--- a/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java
+++ b/springboot/addons/rest-exception-handler/src/main/java/org/kie/kogito/resource/exceptions/springboot/ExceptionsHandler.java
@@ -36,6 +36,7 @@ import org.kie.kogito.resource.exceptions.AbstractExceptionsHandler;
import org.kie.kogito.resource.exceptions.ExceptionBodyMessage;
import org.kie.kogito.usertask.UserTaskInstanceNotAuthorizedException;
import org.kie.kogito.usertask.UserTaskInstanceNotFoundException;
+import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -164,6 +165,11 @@ public class ExceptionsHandler extends AbstractExceptionsHandler<ResponseEntity<
return mapException(exception);
}
+ @ExceptionHandler(UserTaskTransitionException.class)
+ public ResponseEntity<Map<String, String>> toResponse(UserTaskTransitionException exception) {
+ return mapException(exception);
+ }
+
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> toResponse(IllegalArgumentException exception) {
return mapException(exception);
diff --git a/springboot/integration-tests/integration-tests-springboot-wshumantasks-it/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java b/springboot/integration-tests/integration-tests-springboot-wshumantasks-it/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
index e2d069ace2..0f9e1dd736 100644
--- a/springboot/integration-tests/integration-tests-springboot-wshumantasks-it/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
+++ b/springboot/integration-tests/integration-tests-springboot-wshumantasks-it/src/test/java/org/jbpm/usertask/jpa/it/WsHumanTaskLifeCycleIT.java
@@ -685,6 +685,93 @@ public class WsHumanTaskLifeCycleIT {
isProcessCompleted(processId, pid);
}
+ @Test
+ public void testSkipFromCreatedTransition() {
+ var user = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(user, pid);
+ verifyTask(processId, pid, taskId, user, "Created", new String[] {});
+
+ skip(taskId, user);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotSkipFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ skipNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still skip it
+ skip(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testExitFromCreatedTransition() {
+ var admin = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ exit(taskId, admin);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotExitFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ exitNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still exit it
+ exit(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testFaultFromCreatedTransition() {
+ var admin = "carl";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ fault(taskId, admin);
+
+ isProcessCompleted(processId, pid);
+ }
+
+ @Test
+ public void testNonAdminCannotFaultFromCreatedTransition() {
+ var admin = "carl";
+ var nonAdmin = "dave";
+ var processId = "manager_admin";
+ var pid = startProcessInstance(processId);
+ var taskId = getTaskId(admin, pid);
+ verifyTask(processId, pid, taskId, admin, "Created", new String[] {});
+
+ faultNotAuthorized(taskId, nonAdmin);
+
+ // task is still in Created state — admin can still fault it
+ fault(taskId, admin);
+ isProcessCompleted(processId, pid);
+ }
+
@Test
public void testSingleUserUserTaskLifeCycle() {
var user = "jdoe";
@@ -854,6 +941,39 @@ public class WsHumanTaskLifeCycleIT {
.body("status.terminate", equalTo("OBSOLETE"));
}
+ private void skipNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("skip"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
+ private void exitNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("exit"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
+ private void faultNotAuthorized(String taskId, String user) {
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .queryParam("user", user)
+ .body(new TransitionInfo("fault"))
+ .post(USER_TASKS_INSTANCE_TRANSITION_ENDPOINT, taskId)
+ .then()
+ .statusCode(403);
+ }
+
private void resume(String taskId, String user, String previousState) {
given()
.contentType(ContentType.JSON)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org