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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]