troubleshooting red hat process automation manager featured image

I've been around Red Hat JBoss BPM Suite (jBPM) and Red Hat Process Automation Manager (RHPAM) for many years. Over that time, I've learned a lot about the lesser-known aspects of this business process management engine.

If you are like most people, you might believe that user tasks are trivial, and learning about their details is unnecessary. Then, one day, you will find yourself troubleshooting an error like this one:

User '[User:'admin']' was unable to execution operation 'Start' on task id 287271 due to a no 'current status' match.

Receiving one too many similar error messages led me to learn everything that I know about user tasks, and I have decided to share my experience.

User tasks are a vital part of any business process management engine, jBPM included. Their behavior is defined by the OASIS Web Services—Human Task Specification, which has been fully adopted by Business Process Model and Notation (BPMN) 2.0—the standard for business processes diagrams. The spec defines two exceptionally important things that I will discuss in this article: The user task lifecycle and task access control. Without further ado, let's jump right in.

Note: These troubleshooting tips are applicable to Red Hat JBoss BPM Suite 6.2 and above and Red Hat Process Automation Manager 7.

The user task lifecycle

The diagram in Figure 1 illustrates how a task transitions from one state to another, along with the valid executable actions to execute for every state.

Diagram of the user task lifecycle.

To see how this diagram can be helpful, consider a practical example. Imagine a Start - >User Task - > end process. As shown in Figure 2, the task has only one actor assigned, anton.

A simple process for a single user.
Figure 2. A simple process for a single user.

Upon starting this process, the task automatically transitions into the Reserved state, which is dictated by the WS-Human Task spec, section 4.10.1: "When the task has a single potential owner, it transitions into the Reserved state."

Now, let's see what happens when we execute the following call:

$ curl -X PUT -u anton:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/completed?user=anton"
-H "accept: application/json" -H "content-type: application/json" -d "{}"

We will observe this error:

Could not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton]' was unable to execute operation 'Complete' on task id 1 due to a no 'current status' matchCould not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton]' was unable to execute operation 'Complete' on task id 1 due to a no 'current status' match at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.evalCommand(MVELLifeCycleManager.java:163) at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.taskOperation(MVELLifeCycleManager.java:392)

But there's no reason to panic: We can refer to the diagram in Figure 1 to understand what happened. Look closely, and you will see that from the Reserved status, the allowed operation is Start. This moves our task status to InProgress, which lets us execute the Complete operation.

So, to solve this error, we call start and then complete. Or, if you can't be bothered with this task lifecycle mess, use the auto-progress option:

$ curl -X PUT -u anton:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/completed?user=anton&auto-progress=true"
-H "accept: application/json" -H "content-type: application/json" -d "{}"

Internally, the engine falls back to the logic in this implementation example (follow the link for the complete code):

TaskService taskService = engine.getTaskService();
// auto progress if needed
if (task.getStatus().equals(Status.Ready.name())) {
taskService.claim(taskId.longValue(), userId);
taskService.start(taskId.longValue(), userId);
} else if (task.getStatus().equals(Status.Reserved.name())) {
taskService.start(taskId.longValue(), userId);
}
// perform actual operation
taskService.complete(taskId, userId, params);

Based on the current task status, it executes all of the necessary intermediate steps (in our case, that intermediate step was the Start operation). Whether to use autoProgress depends on your client's needs: Some clients require fine-grained distinctions between task states, and some don't care. Using autoProgress certainly simplifies life for you as a developer, but either way, it's important to understand what happens behind the scenes.

Study the source

You might have noticed that the stack trace above mentions useful classes. If you want to go really deep (which I occasionally do), you can dig into these. Low-level implementation details matter for one simple reason: The source code never lies—and the documentation might. Don't trust diagrams, articles, or documentation, for that matter; trust the source code.

The operations-dsl.mvel and MVELLifeCycleManager.java files hold the actual implementation of the task lifecycle illustrated in Figure 1.

Hopefully, this covers the lifecycle-related errors and gives you enough information to help with troubleshooting.

Task access control

The next important aspect of dealing with user tasks is task access control. Essentially, if you want to execute a task-related action, the user must be eligible to execute that action. For a user to be eligible, the engine must consider that user to be a potential owner of the task.

The component that plays a vital role in checking task access is UserGroupCallback. It's a simple interface, and jBPM allows you to plug in various (even custom) implementations. We'll get to UserGroupCallback later.

jBPM task access example

Note that in jBPM, potential owner refers to both individual actors and groups. To illustrate, imagine a task with one group assigned: sampleGroup, shown in Figure 3.

Implementation/Execution: Task Name SampleHT, no actors, Group name sampleGroup
Figure 3: Example process with single group as a potential owner

The task also has two users, which are defined in Red Hat JBoss Enterprise Application Platform as follows:

anton1=kie-server,sampleGroup,admin
anton2=kie-server,admin

Now, we execute a claim operation that is authenticated with the user anton2:

curl -X PUT -u anton2:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/claimed?user=anton2"
-H "accept: application/json"

The claim fails, resulting in the following error:

12:13:20,117 WARN  [org.jbpm.services.task.persistence.TaskTransactionInterceptor] (default task-8) Could not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton2]' does not have permissions to execute operation 'Claim' on task id 112:13:20,117 WARN [org.jbpm.services.task.persistence.TaskTransactionInterceptor] (default task-8) Could not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton2]' does not have permissions to execute operation 'Claim' on task id 1 at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.evalCommand(MVELLifeCycleManager.java:127) at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.taskOperation(MVELLifeCycleManager.java:392) at org.jbpm.services.task.impl.TaskInstanceServiceImpl.claim(TaskInstanceServiceImpl.java:157) at org.jbpm.services.task.commands.ClaimTaskCommand.execute(ClaimTaskCommand.java:52) at org.jbpm.services.task.commands.ClaimTaskCommand.execute(ClaimTaskCommand.java:33)

Now, this error is expected and obvious because anton2 is not part of sampleGroup. The jBPM engine does not consider anton2 a potential owner, and so the operation fails. But let's really try to understand what happened during this call. The stack trace gives us all the clues that we need.

Study the stack trace

First—before the actual claim— UserGroupCallback operation was executed (follow the link for the complete source code):

public Void execute(Context cntxt) {
TaskContext context = (TaskContext) cntxt;
doCallbackUserOperation(userId, context, true);
groupIds = doUserGroupCallbackOperation(userId, null, context);
context.set("local:groups", groupIds);
context.getTaskInstanceService().claim(taskId, userId);
return null;
}

We can see it is passing null as the value for the groups attribute.

This leads us to the next call, which eventually executes the registered UserGroupCallback:

protected List  doCallbackGroupsOperation(String userId, List  groupIds, TaskContext context) {
...
if (!(userGroupsMap.containsKey(userId) && userGroupsMap.get(userId).booleanValue())) {
  //usergroupcallback invocation

List  userGroups = filterGroups(context.getUserGroupCallback().getGroupsForUser(userId));
if (userGroups != null && userGroups.size() > 0) {
for (String group: userGroups) {
addGroupFromCallbackOperation(group, context);
}
userGroupsMap.put(userId, true);
groupIds = userGroups;
}
}
}
}
else {
if (groupIds != null) {
for (String groupId: groupIds) {
addGroupFromCallbackOperation(groupId, context);
}
}
}
return groupIds;
}

We pass userId to the getGroupsForUser callback operation, which results in the list of groups to which our user belongs. By default, jBPM is configured with the JAASUserGroupCallbackImpl callback implementation.

This particular callback implementation delegates the "heavy lifting" to the underlying web container. The container returns the list of groups for the currently authenticated user. The output for the user anton2 will be a list including kie-server,admin.

If we trace the code execution further to MVELLifeCycleManager, the engine will try to find the intersection between potential owners defined on a task (sampleGroup) and the groups that our authenticated user—anton2—belongs to:

private boolean isAllowed(final User user, final List < String > groupIds, final List < OrganizationalEntity > entities) {
for (OrganizationalEntity entity: entities) {
if (entity instanceof User && entity.equals(user)) {
return true;
}
if (entity instanceof Group && groupIds != null && groupIds.contains(entity.getId())) {
return true;
}
}
return false;
}

Our user, anton2, belongs to the kie-server and admin roles. This user does not belong to the sampleGroup. As a result, the engine determines that anton2 is not a potential owner, and the operation fails with a PermissionDeniedException.

Note: There are different callback implementations, and your source of truth doesn't necessarily need to come from the web container. It can come from an LDAP server, a database, a Red Hat Single Sign-On (SSO) instance, and so on. But JAASUSerGroupCallback is a sensible default. If you want to learn more about other out-of-the-box implementations, see the following module with all of its UserGroupCallbacks.

Simplifying user task access control

We have discussed the task lifecycle, task access control, and how user-group callbacks fit into the picture. The relationship between these components presents a challenge, especially when using JAASUserGroupCallback.

Imagine these three tasks:

  • Task1(group1)
  • Task2(group2)
  • Task3(group3)

If you want to claim these tasks, you must execute three separate claim operations (well, unless you implement a kie-server extension to allow multiple claimed tasks at once, but that's a separate topic). Moreover, you must authenticate with three different users, because that is how JAASUserGroupCallback works. If you are using a Java client (kie-server-client), you will also have to create three different instances of KieServicesClient, and you would probably have to cache them to save time.

It's likely that you would end up with something like this:

Map<String,KieServicesClient> clientMap; // String == userId

Let's just say that this code is cumbersome, at best. Fortunately, you can use the following system property to bypass server authentication for a user that is executing a task operation:

org.kie.server.bypass.auth.user = true

This bypass property is a holy grail for our understanding of user task lifecycle and access and will test your knowledge of what you have learned so far.

Note for Spring users: This is a system property, not a Spring property, so don't put it in application.properties.

Using the bypass property

So, what can the final implementation look like when using this property? Again, imagine these three tasks:

  • Task1(group1)
  • Task2(group2)
  • Task3(group3)

And imagine the following users defined in JBoss EAP:

anton1=kie-server,admin,group1
anton2=kie-server,admin,group2
anton3=kie-server,admin,group3
serviceAccount=kie-server,admin

We still have to call claim three separate times. But, with the bypass property enabled, we only have to authenticate once (so, we'll need just one instance of KieServicesClient) and we will pass user1, user2, and user3 in a query parameter. We are effectively bypassing the server authentication for these user instances. Internally, the engine either selects the authenticated user or the user from the query parameter, depending on the value of the bypass property. The selected user is then propagated all the way to the UserGroupCallback. But, again, don't just trust me: Check the source code from the kie-server:

protected String getUser(String queryParamUser) {
if (bypassAuthUser && queryParamUser != null) {
return queryParamUser;
}
return identityProvider.getName();
}

This is literally it: All the magic behind the bypass property. Changing the input parameter to the UserGroupCallback method completely changes the task access control behavior.

Now, let's test further to see if we understand the bypass correctly.

Consider that the previous three tasks are active. Now we want to claim one of these with a bypass enabled on the server-side:

$ curl -X PUT -u serviceAccount:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/claimed?user=anton1"
-H "accept: application/json"

As you can see, we are authenticating with a single user (serviceAccount) and bypassing the authentication of the user requesting the claim (anton1). Let's pause for a second: What do you think will be the outcome of this operation? We have bypass enabled, so user anton1 will go through as an input to the UserGroupCallback. It should work. And yet, it ends with this:

WARN  [org.jbpm.services.task.persistence.TaskTransactionInterceptor] (default task-3) Could not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton1]' does not have permissions to execute operation 'Claim' on task id 1WARN [org.jbpm.services.task.persistence.TaskTransactionInterceptor] (default task-3) Could not commit session: org.jbpm.services.task.exception.PermissionDeniedException: User '[UserImpl:anton1]' does not have permissions to execute operation 'Claim' on task id 1 at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.evalCommand(MVELLifeCycleManager.java:127) at org.jbpm.services.task.internals.lifecycle.MVELLifeCycleManager.taskOperation(MVELLifeCycleManager.java:392

That doesn't make sense, does it? In fact, we've just received one of the most confusing errors in the jBPM world. The anton1 user clearly belongs to group1 So, why does the log say the exact opposite? Why has the world stopped making sense?

The bypass behaves like this due to a tiny implementation detail of JAASUserGroupCallback:

public List getGroupsForUser(String userId) {
List roles = new ArrayList();
try {
Subject subject = getSubjectFromContainer();
...

Although we are passing userId equal to anton1 (which you can see in the error message), the getGroupsForUser method ignores this parameter. It still gets the actual user from the container. As a result, our authenticated user is serviceAccount, but serviceAccount is not a potential owner of this task. There is no intersection between kie-server,admin, and group1, so the call fails.

So, what is the solution? The bypass alone doesn't solve anything for us. We have to use it in combination with either a task administrator or a custom UserGroupCallback.

The task administrator strategy

In jBPM, the task administrator is a superuser that is eligible to execute all task operations, even though it isn't defined as a potential owner. If we combine our existing serviceAccount user with the task administrator's capabilities, we will achieve the behavior that we desire: The user anton1 will inherit the task admin's superpowers from the serviceAccount user.

By default, the task admin is defined as a user with the name of Administrator, or a user who is part of the group Administrators. We can change these two values via the following properties:

public static final String DEFAULT_ADMIN_USER = System.getProperty("org.jbpm.ht.admin.user", "Administrator");
public static final String DEFAULT_ADMIN_GROUP = System.getProperty("org.jbpm.ht.admin.group", "Administrators");

For simplicity, let's add the Administrators role to the serviceAccount user, and then test again:

serviceAccount=kie-server,admin,Administrators

And now this call finally succeeds:

$ curl -X PUT -u serviceAccount:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/claimed?user=anton1"
-H "accept: application/json"

We can now use a single user (in this case, serviceAccount) to authenticate all of our kie-server requests. This dramatically simplifies the integration with the kie-server.

Note: If you are using the kie-server-client API to interact with the kie-server, you need to set the org.kie.server.bypass.auth.user property to true, even on the client-side. Otherwise, the user you want to bypass will not be passed to the queryParameter, and you will end up with confusing behavior again.

Caution

There is an important consequence of combining the task administrator with the bypass property, which should not come as a surprise at this point.

Imagine that we have Task1(group1) and user anton2=kie-server,admin,group2. What do you think will happen after issuing this call?

$ curl -X PUT -u serviceAccount:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/claimed?user=anton2"
-H "accept: application/json"

It will succeed, even though anton2 is not a potential owner (this user is not part of group1). The call succeeds because we have received roles from the authenticated user, which is serviceAccount. This user has the Administrators role set, so our user, anton2, is now a superuser and is thus eligible to execute any action on any task.

Similarly, what do you think will happen after this call?

$ curl -X PUT -u serviceAccount:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/2/states/claimed?user=totallyRandomNonExistentUser"
-H "accept: application/json"

In this case, we pass a user that does not even exist in the container, and the operation still succeeds. If that is unexpected, it shouldn't be. The JAASUserGroupCallbackImpl only looks for the authenticated user (serviceAccount). In this case, the user has superpowers, via the Administrators role. Our totallyRandomNonExistentUser inherits these powers.

The custom UserGroupCallback strategy

If you (or your client) are not willing to accept this behavior, you can choose to use a custom UserGroupCallback, instead of the task admin. For testing purposes, it will be easiest to reuse the already existing application-roles.propertiesJBossUserGroupCallbackImpl is already compatible with this file, so at least we don't have to implement custom logic to parse it. This callback implementation respects the user passed as an input parameter: It ignores the authenticated user and instead returns a list of roles for this user as defined in a properties file.

Once again, you can use LDAP or a database as the source of your user-group mappings. Here's the final configuration:

<property name="org.kie.server.bypass.auth.user" value="true"/>
<property name="jbpm.user.group.mapping" value="file:///Users/agiertli/Documents/work/rhpam77/standalone/configuration/application-roles.properties"/>
<property name="org.jbpm.ht.callback" value="props"/>

The value props is misleading, but it defaults to JBossUserGroupCallbackImpl. Again, if you don't believe me (which you should not), you can double-check the source code:

          } else if ("props".equalsIgnoreCase(USER_CALLBACK_IMPL)) {
callback = new JBossUserGroupCallbackImpl(true);

We can now afford to remove the Administrators role from the serviceAccount, so the application-roles.properties looks like this:

anton1=kie-server,admin,group1
anton2=kie-server,admin,group2
anton3=kie-server,admin,group3
serviceAccount=kie-server,rest-all

And the following call works, as well:

$ curl -X PUT -u serviceAccount:password1!
"http://localhost:8080/kie-server/services/rest/server/containers
/HumanTaskExamples/tasks/1/states/claimed?user=anton1"
-H "accept: application/json"

I hope it's clear why the above configuration works. We pass in anton1 (as opposed to the authenticated user) as the user to the callback. The JBossUserGroupsCallbackImpl then parses the application-roles.properties to give us the list of groups that anton1 belongs to. This list is anton1=kie-server,admin,group1 and the group set on Task1 is group1. Seeing the intersection between these two components, the engine marks the user anton1 as a potential owner for the claim operation, so the operation succeeds.

Conclusion

I hope you now have the sufficient resources to debug, troubleshoot, and configure everything you need in regards to jBPM user tasks. As I've demonstrated with the examples in this article, user tasks can seem trivial, until all of a sudden, they are not. Feel free to share your user task troubleshooting stories in the comments!

Last updated: September 21, 2020