BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Authorizing Process Access and Execution with JBoss jBPM

Authorizing Process Access and Execution with JBoss jBPM

One of the common BPM trends today is centralizing BPM executions across the company or large enough division of the company. This means that a single BPM server (cluster) runs many instances of many process definitions across the company. The challenge of such an approach is that although BPM engines (including jBPM) typically provide authorization of the task access[1], they typically do not provide authorization for viewing and deleting process definitions, starting, ending, viewing and deleting process instances, etc. In this article we will describe how to extend jBPM engine (based on jBPM 4.3) to implement this functionality.

Overall Implementation approach

The overall implementation approach is fairly simple and straightforward – for every process definition introduce (similar to task definitions) a list of users/user groups authorized to work with definitions, instances and history for a given process. Furthermore we want to support multiple authorization levels for a given user/group – currently we are introducing two roles: “starter” and “user”. Here starter is the role that allows to do anything with process definitions/instances/histories, while user role is limited to querying process/history permissions.

Implementation of such approach requires the following changes:

  • Process Definition
    • Adding process access permissions to the process definition
  • Process Deployment
    • Extending of the current process deployer for parsing process authorization definitions and building process access list
    • Introduction of additional class/database table storing access permission for every process definition
  • Process Execution
    • Introduction of authorized commands – commands requiring user to be authorized for their execution.
    • Modifying existing jBPM that we want to be authorized based on the current user’s credentials. These include start, end and delete process instance and delete deployment definitions.
    • Modifying existing jBPM queries to take into consideration existing user’s credentials. These include deployment and process definition query, process instances query, and history process instance, activity and details query.

In addition to the above we also want to extend process instance query to allow user to narrow the search result by specifying certain process variables values. One of the common cases for such search is querying for processes “started by me”. To ensure that this query is always available our updated implementation of the start process instance command transparently adds current user ID to the set of the process variables.

Finally, in order to be able to support multiple methods of user authentication, we have implemented a custom identity session implementation, that supports programmatic setting and access of current user’s credentials. The intent here is to separate obtaining user credentials – ID and groups participation – from usage of this information by jBPM runtime.

Our implementation leverages a very powerful and flexible jBPM 4 configuration mechanism, which allowed us to:

  • Minimize the amount of the custom code through extending existing jBPM classes and implementing only additional functionality, required for our extensions
  • Implement our extensions as a separate jar that can be used together with the standard jBPM 4 libraries and does not require any changes to the libraries.

Before delving in the details of our implementation we will first discuss jBPM 4 configurations that we heavily used.

jBPM 4 configuration mechanisms

jBPM is based on the process virtual machine (PVM)[2], which is build on custom dependency injection implementation. Dependency injection is control by a very powerful, XML-based configuration mechanism used for creation of binding between tags and specific implementations, adhering to the predefined interfaces.

At the heart of this mechanism is jbpm.wire.bindings.xml file describing[3] major components of the jBPM PVM, including:

  • Basic types
  • Object and references
  • Environment referencing
  • Hibernate bindings
  • Sessions
  • Services
  • Deployers
  • Interceptors
  • Etc.

This file is part of JBPM distribution. If a user wants to add his own bindings, instead of modifying jbpm.wire.bindings.xml file he can create jbpm.user.wire.bindings.xml, describing them.

Both files are read and parsed by jBPM PVM at startup and serves as a foundation PVM execution configuration, defined in file jbpm.cfg.xml. This file typically contains multiple parts describing configuration of the specific components of PVM execution.

jBPM PVM is comprised of a set of services that provide PVM functionality[4]. Main PVM services include:

  • Repository service providing a set of methods to view and manage the repository of deployments
  • Execution service providing a set of methods to view and manage running process executions.
  • Management service providing a set of methods to view and manage jobs
  • Task service providing a set of methods to view and manage user tasks.
  • History service providing a set of methods to provides access to the history information on both running and completed process executions.

The list of available services and classes implementing these services (using binding described above) is configured as process engine context.

Services execution is implemented as a set of commands, that get invoked as part of executions of service methods. The actual execution of commands is controlled by command service.

The command service is configured in the command service context as a set of interceptors, implementing cross-cutting concerns, around command invocation (command execution pipeline). Default jBPM distribution comes with the following interceptors in the command pipeline:

  • Retry interceptor is responsible for retrying command execution
  • Environment-interceptor is responsible for injecting, if required, jBPM context into command execution
  • Transaction interceptor is responsible for implanting transactional demarcation of command invocations.

Interceptors are the core mechanism for porting jBPM to different environments and/or introduction of additional cross-cutting concerns.

Command execution typically leverage environment, which is also configured. Typical components of environment are:

  • Repository session
  • Db session
  • Message session
  • Timer session
  • History sessions
  • Mail session

Additional sessions can be added to extend PVM functionality.

Finally, deployment manager configuration allows specifying a set of deployers, that are executed sequentially to deploy business processes into PVM. Such an approach allows to implement additional deployment steps for extended process definition rather then overwriting a deployer that is shipped with jBPM distribution.

Overall PVM architecture[5] is presented at Figure 1

Figure 1 PVM Architecture

Introducing Authorization in the process definition

As we can see (Figure 2) allows adding any attributes to the process definitions. Using this extensibility option, we can now define the following process attributes, describing its authorization policies:

  • starter-users – list of users with the starter role;
  • starter-groups – list of groups with the starter role;
  • user-users – list of users with the user role;
  • user-groups – list of groups with the user role.

The value of each attribute is a comma-separated list of group/user ids.

Figure 2 Process Definition schema

Additionally we are defining a special user type – “any” and two user groups – “all” and “admin”. Any user, regardless of what his real ID is also “any” user. Any group, regardless of its ID is also “all”. Finally member of “admin” group is considered to be a member of any group.

Process authorization definition is driven by following rules:

  • If neither user-users, nor user-groups are specified, then user-users=”all”
  • If neither starter-users, nor starte-groups are specified, then process users are additionally assigned starter role.

Based on this, a process (Listing 1) can be both started and used by anyone

<process package="com.navteq.jbpm" 
key="NO_AUTHORIZATION"
name="Test Authorization not required" version="1"
xmlns="http://jbpm.org/4.0/jpdl">
<start g="68,14,48,48" name=
"start" > <transition to="end"/> </start>

<end g="78,383,48,48" name="end"/>
</process>

Listing 1 Process with no authorization

A process (Listing 2) can be both used and started by mark or anyone in the group tomcat.

<process package="com.navteq.jbpm"

<process package="com.navteq.jbpm" 	    key="AUTHORIZATION" 	    name="Test Authorization Required" version="1" 	    xmlns="http://jbpm.org/4.0/jpdl" 	    user-users="mark" 	    user-groups="tomcat">
<start g="68,14,48,48" name="start" >
<transition to="end"/>
</start>
<end g="78,383,48,48" name="end"/>
</process>

Listing 2 Process with user authorization

We are introducing a new class – ACL, which contains an individual access list (user or group and type) for a given process (processDefinitionID, processDefinitionKey, DeploymentID) and corresponding hibernate definition.

Deploying of the process (Listing 1) creates 2 ACL for the user “any” with 2 roles – “user” and “starter” (Figure 3), while deploying of process (Listing 2) will create 4 – user “mark”, group “tomcat” with 2 roles – “user” and “starter” (Figure 4).

Figure 3 ACLs for the process with no authorization

Figure 4 ACLs for the process with user authorization

Population of the ACLs is done through introduction of an additional deployer, which runs after “standard” jBPM deployer, extracts authorization attributes described above and builds ACLs for a given process.

Securing jBPM commands

A general approach to securing jBPM commands, that we adopted includes implementation of custom annotations for defining command requiring authorization and a custom authorization session (command interceptor) implementation processing this annotation.

Authorization annotations (Listing 3) allow specifying required user role and the way a process is represented.

@Retention(value=RetentionPolicy.RUNTIME)
@Target(value=ElementType.METHOD)
public @interface AuthorizedCommand {
	/** Access type */
	public String role();
	String key();
}

Listing 3 Authorization annotation

A user role - “starter” or “user” – refers to the role given user should have for a given process[6]. Because different command can refer to either deploymentID or process ID or process key an annotation allows specifying an appropriate reference as a key.

An implementation of an authorization interceptor (Listing 4) checks whether any of the command’s methods is decorated with the authorization annotation. If annotation is found, then appropriate queries are executed to determine a set of users/user groups are authorized for a given command and check whether current user belong to them.

…………..
@SuppressWarnings("unchecked")
public void checkPermission(Command<?> command, EnvironmentImpl environment) {
	
environment.setAuthenticatedUserId(environment.get(AuthorizationIdentitySession.class).getAuthenticatedUserId());
	for( Method method : command.getClass().getMethods()) {
		AuthorizedCommand sc = method.getAnnotation(AuthorizedCommand.class);
		if(sc != null){
			log.debug("Checking Class based Secured Function");
			String ID = environment.get(AuthorizationIdentitySession.class).getAuthenticatedUserId();
			Object value = null;
			try {
				log.debug("Checking authorization: " + command.getClass().getName());
				Session session = environment.get(SessionImpl.class);
				value = method.invoke(command, (Object[])null);
				Query uQ = session.createQuery(userQuery.get(sc.key())).
					setString("role", sc.role()).setString("value",(String) value);
				Query gQ = session.createQuery(groupQuery.get(sc.key())).
					setString("role", sc.role()).setString("value", (String) value);
				List<String> userIds = (List<String>)uQ.list();
				List<String> groups = (List<String>)gQ.list();
				if(!isAuthorized(environment, userIds, groups)) 
					throw new AccessControlException(ID+" attempted access to ProcessDefinition #"+value);
			} catch (IllegalArgumentException e) {
				log.error("Caught " + IllegalArgumentException.class, e);
				throw new AccessControlException(ID+" attempted access to ProcessDefinition #"+value);
			} catch (IllegalAccessException e) {
				log.error("Caught " + IllegalAccessException.class, e);
				throw new AccessControlException(ID+" attempted access to ProcessDefinition #"+value);
			} catch (InvocationTargetException e) {
				log.error("Caught " + InvocationTargetException.class, e);
				throw new AccessControlException(ID+" attempted access to ProcessDefinition #"+value);
			}
		}  
	}
	return;
}
……………………..
public boolean isAuthorized(EnvironmentImpl env, List<String> authorizedUserIds, List<String> authorizedGroupIds) {
	AuthorizationIdentitySession identitySession = env.get(AuthorizationIdentitySession.class);
	if (authorizedUserIds.contains(AuthorizationIdentitySession.ANONYMOUS_USER_ID)) 
		return true;
	if (authorizedUserIds.contains(identitySession.getAuthenticatedUserId()) ) 
		return true;
	//check if any of userGroups is an authorized group.  if so then return true
	List<Group> groups = identitySession.findGroupsByUser(identitySession.getAuthenticatedUserId());
	for(Group group : groups){
		String g = group.getId();
		// admin is allowed to execute any command
						if(g.equals(AuthorizationIdentitySession.ADMINISTRATORS_GROUP))
			return true;
		if(authorizedGroupIds.contains(g))
			return true;
}
return false;
}

Listing 4 Authorization interceptor

In order to make a command implementation secure, we create a new class extending existing command and adding an annotated method (Listing 5) returning a key available for a given command.

@AuthorizedCommand(role = ACL.STARTER, key = NavteqAuthorizationSession.PROCESSID)
public String getProcessDefinitionKey() {
   return processDefinitionId;
}

Listing 5 Introducing authorization to start process instance command

Based on the proposed approach we have annotated the following commands:

  • Delete deployment
  • Start process instance
  • Start latest process instance
  • End process instance
  • Delete process instance

Extending Queries

Introduction authorization means that results of the query should return only information that user is authorized for[7]. This can be achieved by extending a where clause for the existing queries (Listing 6)

//check authorization
String userId = EnvironmentImpl.getCurrent().getAuthenticatedUserId();
List<Group> userGroups = EnvironmentImpl.getCurrent().get(IdentitySession.class).findGroupsByUser(userId);

hql.append(", " + ACL.class.getName() + " as acl ");
appendWhereClause("acl.deployment=deployment and acl.type='"
	+ ACL.STARTER + "' ", hql);
appendWhereClause("((acl.userId in "
	+ Utils.createHqlUserString(userId) + ") or " + "(acl.groupId in "
					+ Utils.createHqlGroupString(userGroups) + ")) ", hql);

Listing 6 Additional where clause for supporting authorization for process definition query

Additional where clause is implemented by extending existing query implementation and overwriting hlq method.

The following queries have been extended based on this approach:

  • Deployment query
  • Process definitions query
  • Process instances query
  • History process instances query
  • History activity query
  • History detail query

We also additionally extended a process instance query to be able to add string instance variables to further narrow query results

Supporting arbitrary user management approaches

The implementation presented above relies on the usage of the execution environment for obtaining current user ID and IdentitySession for obtaining user groups membership. jBPM distribution comes with 2 implementations of this interface:

  • IdentitySessionImpl is based on jBPM users/groups database
  • JBossIdmIdentitySessionImpl is based on JBoss Identity IDM component,

Instead of rolling out additional technology dependent implementation, for our implementation we decided to separate obtaining user ID/groups from storing this information and make it available to the rest of jBPM implementation (Figure 5).

Figure 5 User management implementation

To ensure that environment is available during setting/resetting of user credentials we have implemented these two operations as commands (Listing 7), thus relying on jBPM command execution service to set the execution environment correctly.

public static class SetPrincipalCommand extends AbstractCommand<Void> {
	private static final long serialVersionUID = 1L;
	private String userId;
	private String[] groups;
	public SetPrincipalCommand(String u, String...groups) { this.userId=u; this.groups=groups; }
	public Void execute(Environment environment) throws Exception {
		environment.get(AuthorizationIdentitySession.class).setPrincipal(userId,groups);
		return null;
	}
}

public static class ResetPrincipalCommand extends AbstractCommand<Void> {
	private static final long serialVersionUID = 1L;
	public Void execute(Environment environment) throws Exception {
		environment.get(AuthorizationIdentitySession.class).reset();
		return null;
	}
}

Listing 7 Set user credentials commands

Including new commands and queries into jBPM execution

Because jBPM does not provide any support for configuring commands - services relationships, in order to change a command in a given service it is necessary to overwrite a service implementation with a new one invoking a new command. An example of overwriting history service with new commands is presented at Listing 8.

public class NavteqHistoryServiceImpl extends HistoryServiceImpl {

	@Override
	public HistoryActivityInstanceQuery createHistoryActivityInstanceQuery() {
		return new AuthorizedHistoryActivityInstanceQueryImpl(commandService);
	}

	@Override
	public HistoryDetailQuery createHistoryDetailQuery() {
		return new AuthorizedHistoryDetailQueryImpl(commandService);
	}

	@Override
	public HistoryProcessInstanceQuery createHistoryProcessInstanceQuery() {
		return newAuthorizedHistoryProcessInstanceQuery(commandService);
	}
}

Listing 8 Introduction of authorization commands in history service

Conclusion

The bulk of implementation[8] presented in this article extends not JPDL, but rather JBoss PVM which is used by all jBPM supported languages[9] . As a result it can be leveraged by any of them.

Acknowledgements

I am thankful to my NAVTEQ colleagues, especially Mark Kedzierski for discussions on overall design and implementation of majority of code and Stefan Balkowec, Vlad Zhukov, Eugine Felds and Catalin Capota for help in defining the overall solution.


[1] Based on the user credentials – user ID and groups

[2] Tom Baeyens on the Process Virtual Machine ; Tom Baeyens and Miguel Valdes Faura - The Process Virtual Machine.

[3] The file contains not the bindings themselves, but rather names of java classes, defining bindings

[4] http://docs.jboss.com/jbpm/v4/userguide/html_single/

[5] http://docs.jboss.com/jbpm/v4/devguide/html_single/

[6] See above

[7] A useful side effect of this approach is that it limits the amount of information that is returned to the user as a query result.

[8] With the exception of the extended JPDL parser

[9] Currently JPDL and BPMN

Rate this Article

Adoption
Style

BT