Wednesday, February 15, 2012

Activiti Authentication And Identity Management Tutorial

This time we focus on Business Process Management (BPM) and more precisely on Activiti. Activiti is an open source process engine for Java, that we decided to look into. One of the questions that arose during development was how to assimilate our users and groups into the Activiti platform. Luckily, this is supported by the Activiti engine, but not so well documented (to our knowledge). We have decided to take on the challenge, and create a quick tutorial on how to manage users and groups identity in Activiti (and more precisely Activiti 5.8).

Activiti supports two main entry points that you need to implement in order to integrate your identity solution. These are the user manager and the group manager classes. We will first create a factory for group/user management, and then implement the class that we create in the factory. Lets take a dive into the code, implementing the MyGroupManagerFactory class first:

   1: package my.great.company.com.businessprocessengine.identityservice;

   2:  

   3: import org.activiti.engine.impl.interceptor.Session;

   4: import org.activiti.engine.impl.interceptor.SessionFactory;

   5: import org.activiti.engine.impl.persistence.entity.GroupManager;

   6:  

   7: public class MyGroupManagerFactory implements SessionFactory {

   8:  

   9:     private MyConnectionParams    connectionParams;

  10:  

  11:     public MyGroupManagerFactory(MyConnectionParams params) {

  12:         this.connectionParams = params;

  13:     }

  14:  

  15:     @Override

  16:     public Class<?> getSessionType() {

  17:         return GroupManager.class;

  18:     }

  19:  

  20:     @Override

  21:     public Session openSession() {

  22:         return new MyGroupManager(connectionParams);

  23:     }

  24: }
We will also want to provide a factory for user management classes, so here is the MyUserManagerFactory :
   1: package my.great.company.com.businessprocessengine.identityservice;

   2:  

   3: import org.activiti.engine.impl.interceptor.Session;

   4: import org.activiti.engine.impl.interceptor.SessionFactory;

   5: import org.activiti.engine.impl.persistence.entity.UserManager;

   6:  

   7: public class MyUserManagerFactory implements SessionFactory {

   8:  

   9:     private MyConnectionParams connectionParams;

  10:     

  11:     public MyUserManagerFactory(MyConnectionParams params) {

  12:         this.connectionParams = params;

  13:     }

  14:     

  15:     @Override

  16:     public Class<?> getSessionType() {

  17:       return UserManager.class;

  18:     }

  19:  

  20:     @Override

  21:     public Session openSession() {

  22:       return new MyUserManager(connectionParams);

  23:     }

  24:  

  25: }
As you can see above we assume that we want to get connection prams in the instantiation of the classes (assume MyConnectionParams is a simple POJO, and implement it for your needs). Moreover, you will need the Activiti engine jar file in your classpath.

We now implement MyGroupManager and MyUserManager classes. We did not not detail the entire code, but created an example to show what must be implemented and what you can skip. In MyGroupManager  we only implemented the findGroupByQueryCriteria, and findGroupCountByQueryCriteria. Methods that are marked with TODO notation are mandatory in order to work with Activiti core engine. If you want to work with Activity Explorer - implement all the methods.
   1: package my.great.company.com.businessprocessengine.identityservice;
   2:  

   3: import java.util.ArrayList;

   4: import java.util.List;

   5:  

   6: import org.activiti.engine.ActivitiException;

   7: import org.activiti.engine.identity.Group;

   8: import org.activiti.engine.impl.GroupQueryImpl;

   9: import org.activiti.engine.impl.Page;

  10: import org.activiti.engine.impl.persistence.entity.GroupEntity;

  11: import org.activiti.engine.impl.persistence.entity.GroupManager;

  12: import org.apache.commons.lang.StringUtils;

  13:  

  14: public class MyGroupManager extends GroupManager {

  15:  

  16:     

  17:     public MyGroupManager(MyConnectionParams connectionParams) {

  18:         

  19:     }

  20:  

  21:     @Override

  22:     public Group createNewGroup(String groupId) {

  23:         throw new ActivitiException("My group manager doesn't support creating a new group");

  24:     }

  25:  

  26:     @Override

  27:     public void insertGroup(Group group) {

  28:         throw new ActivitiException("My group manager doesn't support inserting a new group");

  29:     }

  30:  

  31:     @Override

  32:     public void updateGroup(Group updatedGroup) {

  33:         throw new ActivitiException("My group manager doesn't support updating a new group");

  34:     }

  35:  

  36:     @Override

  37:     public void deleteGroup(String groupId) {

  38:         throw new ActivitiException("My group manager doesn't support deleting a new group");

  39:     }
  40:  

  41:     @Override

  42:     public long findGroupCountByQueryCriteria(Object query) {

  43:         return findGroupByQueryCriteria(query, null).size();

  44:     }

  45:  

  46:     @Override

  47:     public List<Group> findGroupByQueryCriteria(Object query, Page page) {

  48:         List<Group> groupList = new ArrayList<Group>();

  49:         GroupQueryImpl groupQuery = (GroupQueryImpl) query;

  50:         if (StringUtils.isNotEmpty(groupQuery.getId())) {

  51:             GroupEntity singleGroup = findGroupById(groupQuery.getId());

  52:             groupList.add(singleGroup);

  53:             return groupList;

  54:         } else if (StringUtils.isNotEmpty(groupQuery.getName())) {

  55:             GroupEntity singleGroup = findGroupById(groupQuery.getId());

  56:             groupList.add(singleGroup);

  57:             return groupList;

  58:         } else if (StringUtils.isNotEmpty(groupQuery.getUserId())) {

  59:             return findGroupsByUser(groupQuery.getUserId());

  60:         } else {

  61:             //TODO: get all groups from your identity domain and convert them to List<Group>

  62:             return null;

  63:         } //TODO: you can add other search criteria that will allow extended support using the Activiti engine API

  64:     }

  65:  

  66:     @Override

  67:     public GroupEntity findGroupById(String activitiGroupID) {

  68:         //TODO

  69:         throw new ActivitiException("My group manager doesn't support finding a group");

  70:     }

  71:  

  72:     @Override

  73:     public List<Group> findGroupsByUser(String userLogin) {

  74:         //TODO

  75:         throw new ActivitiException("My group manager doesn't support finding a group");

  76:     }

  77: }
In MyUserManager, we have implemented findUserCountByQueryCriteria and findUserByQueryCriteria. Again - the methods that are marked with TODO notation are mandatory in order to work with Activiti core engine.

   1: package my.great.company.com.businessprocessengine.identityservice;

   2:  

   3: import java.util.ArrayList;

   4: import java.util.List;

   5:  

   6: import org.activiti.engine.ActivitiException;

   7: import org.activiti.engine.identity.User;

   8: import org.activiti.engine.impl.Page;

   9: import org.activiti.engine.impl.UserQueryImpl;

  10: import org.activiti.engine.impl.persistence.entity.UserEntity;

  11: import org.activiti.engine.impl.persistence.entity.UserManager;

  12: import org.apache.commons.lang.StringUtils;

  13:  

  14: public class MyUserManager extends UserManager {

  15:     

  16:     public MyUserManager(MyConnectionParams connectionParams) {

  17:     }

  18:  

  19:     @Override

  20:     public User createNewUser(String userId) {

  21:         throw new ActivitiException("My user manager doesn't support creating a new user");

  22:     }

  23:  

  24:     @Override

  25:     public void insertUser(User user) {

  26:         throw new ActivitiException("My user manager doesn't support inserting a new user");

  27:     }

  28:  

  29:     @Override

  30:     public void updateUser(User updatedUser) {

  31:         throw new ActivitiException("My user manager doesn't support updating a user");

  32:     }

  33:  

  34:     @Override

  35:     public void deleteUser(String userId) {

  36:         throw new ActivitiException("My user manager doesn't support deleting a user");

  37:     }

  38:  

  39:     @Override

  40:     public UserEntity findUserById(String userLogin) {

  41:         //TODO: get my user according to userLogin and convert it to UserEntity

  42:         throw new ActivitiException("My user manager doesn't support finding a user");

  43:     }

  44:  

  45:     @Override

  46:     public List<User> findUserByQueryCriteria(Object query, Page page) {

  47:  

  48:         List<User> userList = new ArrayList<User>();

  49:         UserQueryImpl userQuery = (UserQueryImpl) query;

  50:         if (StringUtils.isNotEmpty(userQuery.getId())) {

  51:             userList.add(findUserById(userQuery.getId()));

  52:             return userList;

  53:         } else if (StringUtils.isNotEmpty(userQuery.getLastName())) {

  54:             userList.add(findUserById(userQuery.getLastName()));

  55:             return userList;

  56:         } else {

  57:             //TODO: get all users from your identity domain and convert them to List<User>

  58:             return null;

  59:         } //TODO: you can add other search criteria that will allow extended support using the Activiti engine API

  60:     }

  61:  

  62:     @Override

  63:     public long findUserCountByQueryCriteria(Object query) {

  64:         return findUserByQueryCriteria(query, null).size();

  65:     }

  66:  

  67:     @Override

  68:     public Boolean checkPassword(String userId, String password) {

  69:         //TODO: check the password in your domain and return the appropriate boolean

  70:         return false;

  71:     }

  72: }
The only thing left to do is to inject (using Spring) our classes to the Activity engine configuration. We will need to inject MyConnectionParams as well as the factories themselves. Find your configuration file and add:
   1: <?xml version="1.0" encoding="UTF-8"?>

   2: <beans xmlns="http://www.springframework.org/schema/beans"

   3:     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

   4:     xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">

   5:  

   6:     <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">

   7:  

   8:         ...

   9:         <property name="customSessionFactories">

  10:             <list>

  11:                 <bean

  12:                     class="my.great.company.com.businessprocessengine.identityservice.MyUserManagerFactory">

  13:                     <constructor-arg ref="MyConnectionParams" />

  14:                 </bean>

  15:                 <bean

  16:                     class="my.great.company.com.businessprocessengine.identityservice.MyGroupManagerFactory">

  17:                     <constructor-arg ref="MyConnectionParams" />

  18:                 </bean>

  19:             </list>

  20:         </property>

  21:         ...

  22:     </bean>

  23:    

  24:     <bean id="MyConnectionParams"

  25:         class="my.great.company.com.businessprocessengine.identityservice.MyConnectionParams">

  26:     </bean>

  27:     ...

  28:  

  29: </beans>


That’s it. You can now use your own users and groups to create processes, deploy them, login to the Activiti Explorer or Rest API, etc.

So, here are a few extra pointers about Activiti Identity:
  • First of all there are 3 predefined group types:
    • activity-role
    • assignment
    • user
  • Now, no one tells you this, but if you are using Activiti Explorer, and you want your user to have admin privileges (i.e. be able to see the 'manage' tab), then the user's group type must be 'security-role' and the user's group name must be hard coded 'admin'. This means you will need to create some special code hacking for special users to use the Activiti Explorer.
  • Lets say you created a process that has a user task with a candidate group option (denote said group as X). NOTE: If you start a process instance, than only if the type of the group X is 'assignment' then users that belong to X will be able to claim the task. Otherwise, the user will not be able to claim the task at all.


12 comments:

  1. Hello, thanks for this wonderful post. It's very useful as this is what we're trying inject workflow into our application. Do you happen to have the full source for this, maybe something more elaborate? I'm new to activity a more detailed example would be extremely helpful. Activiti it's light weight enough for us to use in our application. Thanks again, Paul

    ReplyDelete
  2. Hello, very intresting post. But I'm more interrested in start process intance !
    I would like to start a new instance of a process from a java application.
    How I can assign a user or user group in my process intance?
    Thanks for help.

    ReplyDelete
  3. IdentityService already provides API to add,delete User and groups

    ReplyDelete
  4. Hi ,very useful post.but what should MyConnectionParams class contain.

    ReplyDelete
  5. As we wrote:
    "assume MyConnectionParams is a simple POJO, and implement it for your needs"

    ReplyDelete
  6. Thank you very much for the quick reply.

    ReplyDelete
  7. Hi Roi Gamliel ,
    I have deployed activiti-explorer webapp and my web application which is developed on springs framework on the same tomcatserver and i have configured SpringProcessEngineConfiguration as the way you have said above , and my configuration code is as follows


















    but when i log in into activiti explorer it is still referencing it's own users and groups table.In the myusermanger class i am returning always true in the checkpassword function,so in the activitiexplorer even though i give wrong password it must let me login. what could be the problem.
    Please help me out to fix this issue.

    ReplyDelete
  8. Which configuration file should I modify in the standard demo installation? I tred with apps\apache-tomcat-6.0.32\webapps\activiti-explorer\WEB-INF\applicationContext.xml, but something went wrong :-(

    Thank you very much!

    Franco

    ReplyDelete
    Replies
    1. Don't worry, now it works: I din't get that the "getSessionType" methods must return the UserManager/GroupManager class in order to be recognized by the Activiti engine.

      Thanks

      Franco

      Delete
  9. This comment has been removed by the author.

    ReplyDelete
  10. I am trying to complete a user task for which fozzie is the assignee.
    The code is :

    TaskService taskService = processEngine.getTaskService();
    List tasks = taskService.createTaskQuery().taskAssignee("fozzie")?.list();
    taskService.complete(tasks.get(0).getId(),variables);

    But exception occurrs as follows :

    rg.activiti.engine.ActivitiException: Provided id is null

    at org.activiti.engine.impl.UserQueryImpl.userId(UserQueryImpl.java:53)

    at Script1.run(Script1.groovy:4)

    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:315)

    at org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.eval(GroovyScriptEngineImpl.java:111)

    at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:216)

    I tried
    IdentityService identityService = processEngine.getIdentityService();
    identityService.setAuthenticatedUserId("fozzie")
    But it does not work.
    I guess i am unable to create a session with user as fozzie.

    Can you please help in this regard.

    ReplyDelete
  11. I'm using Activiti Community version 6 and i followed exactly the tutorial and replaced few class names like UserManager and GroupManager with UserEntityManager and GroupEntityManager as per new version. And also I created applicationContext.xml in /../WEB-APPS/classes/ which is actually missing (don't know about the issue why was it missing).
    What does MyConnectionParam class exactly contain?

    But still i could login and create users in Act_id_user. How will i know whether it is actually accessing my custom database table?

    ReplyDelete