Activiti with Spring Security

1. Overview

Activiti is an open-source BPM (Business Process Management) system. For an introduction, check our Guide to Activiti with Java.

Both Activiti and the Spring framework provide their own identity management. However, in an application that integrates both projects, we may want to combine the two into a single user management process.

In the following, we’ll explore two possibilities to achieve this: one is by providing an Activiti-backed user service for Spring Security and the other by plugging a Spring Security user source into the Activiti identity management.

2. Maven Dependencies

To set up Activiti in a Spring Boot project, check out our previous article. In addition to activiti-spring-boot-starter-basic, we’ll also need the activiti-spring-boot-starter-security dependency:

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter-security</artifactId>
    <version>6.0.0</version>
</dependency>

3. Identity Management Using Activiti

For this scenario, the Activiti starters provide a Spring Boot auto-configuration class which secures all REST endpoints with HTTP Basic authentication.

The auto-configuration also creates a UserDetailsService bean of class IdentityServiceUserDetailsService.

The class implements the Spring interface UserDetailsService and overrides the loadUserByUsername() method. This method retrieves an Activiti User object with the given id and uses it to create a Spring UserDetails object.

Also, the Activiti Group object corresponds to a Spring user role.

What this means is that when we login to the Spring Security application, we’ll use Activiti credentials.

3.1. Setting Up Activiti Users

First, let’s create a user in an InitializingBean defined in the main @SpringBootApplication class, using the IdentityService:

@Bean
InitializingBean usersAndGroupsInitializer(IdentityService identityService) {
    return new InitializingBean() {
        public void afterPropertiesSet() throws Exception {
            User user = identityService.newUser("activiti_user");
            user.setPassword("pass");
            identityService.saveUser(user);

            Group group = identityService.newGroup("user");
            group.setName("ROLE_USER");
            group.setType("USER");
            identityService.saveGroup(group);
            identityService.createMembership(user.getId(), group.getId());
        }
    };
}

You’ll notice that since this will be used by Spring Security, the Group object name has to be of the form “ROLE_X”.

3.2. Spring Security Configuration

If we want to use a different security configuration instead of the HTTP Basic authentication, first we have to exclude the auto-configuration:

@SpringBootApplication(
  exclude = org.activiti.spring.boot.SecurityAutoConfiguration.class)
public class ActivitiSpringSecurityApplication {
    // ...
}

Then, we can provide our own Spring Security configuration class that uses the IdentityServiceUserDetailsService to retrieve users from the Activiti data source:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IdentityService identityService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
      throws Exception {

        auth.userDetailsService(userDetailsService());
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new IdentityServiceUserDetailsService(
          this.identityService);
    }

    // spring security configuration
}

4. Identity Management Using Spring Security

If we already have user management set up with Spring Security and we want to add Activiti to our application, then we need to customize Activiti’s identity management.

For this purpose, there’re two main classes we have to extend: UserEntityManagerImpl and GroupEntityManagerImpl which handle users and groups.

Let’s take a look at each of these in more detail.

4.1. Extending UserEntityManagerImpl

Let’s create our own class which extends the UserEntityManagerImpl class:

public class SpringSecurityUserManager extends UserEntityManagerImpl {

    private JdbcUserDetailsManager userManager;

    public SpringSecurityUserManager(
      ProcessEngineConfigurationImpl processEngineConfiguration,
      UserDataManager userDataManager,
      JdbcUserDetailsManager userManager) {

        super(processEngineConfiguration, userDataManager);
        this.userManager = userManager;
    }

    // ...
}

This class needs a constructor of the form above, as well as the Spring Security user manager. In our case, we’ve used a database-backed UserDetailsManager.

The main methods we want to override are those that handle user retrieval: findById(), findUserByQueryCriteria() and findGroupsByUser().

The findById() method uses the JdbcUserDetailsManager to find a UserDetails object and transform it into a User object:

@Override
public UserEntity findById(String userId) {
    UserDetails userDetails = userManager.loadUserByUsername(userId);
    if (userDetails != null) {
        UserEntityImpl user = new UserEntityImpl();
        user.setId(userId);
        return user;
    }
    return null;
}

Next, the findGroupsByUser() method finds all the Spring Security authorities of a user and returns a List of Group objects:

public List<Group> findGroupsByUser(String userId) {
    UserDetails userDetails = userManager.loadUserByUsername(userId);
    if (userDetails != null) {
        return userDetails.getAuthorities().stream()
          .map(a -> {
            Group g = new GroupEntityImpl();
            g.setId(a.getAuthority());
            return g;
          })
          .collect(Collectors.toList());
    }
    return null;
}

The findUserByQueryCriteria() method is based on a UserQueryImpl object with multiple properties, from which we’ll extract the group id and user id, as they have correspondents in Spring Security:

@Override
public List<User> findUserByQueryCriteria(
  UserQueryImpl query, Page page) {
    // ...
}

This method follows a similar principle to the ones above, by creating User objects from UserDetails objects. See the GitHub link at the end for the full implementation.

Similarly, we’ve got the findUserCountByQueryCriteria() method:

public long findUserCountByQueryCriteria(
  UserQueryImpl query) {

    return findUserByQueryCriteria(query, null).size();
}

The checkPassword() method should always return true as the password verification is not done by Activiti:

@Override
public Boolean checkPassword(String userId, String password) {
    return true;
}

For other methods, such as those dealing with updating users, we’ll just throw an exception since this is handled by Spring Security:

public User createNewUser(String userId) {
    throw new UnsupportedOperationException("This operation is not supported!");
}

4.2. Extend the GroupEntityManagerImpl

The SpringSecurityGroupManager is similar to the user manager class, except the fact it deals with user groups:

public class SpringSecurityGroupManager extends GroupEntityManagerImpl {

    private JdbcUserDetailsManager userManager;

    public SpringSecurityGroupManager(ProcessEngineConfigurationImpl
      processEngineConfiguration, GroupDataManager groupDataManager) {
        super(processEngineConfiguration, groupDataManager);
    }

    // ...
}

Here the main method to override is the findGroupsByUser() method:

@Override
public List<Group> findGroupsByUser(String userId) {
    UserDetails userDetails = userManager.loadUserByUsername(userId);
    if (userDetails != null) {
        return userDetails.getAuthorities().stream()
          .map(a -> {
            Group g = new GroupEntityImpl();
            g.setId(a.getAuthority());
            return g;
          })
          .collect(Collectors.toList());
    }
    return null;
}

The method retrieves a Spring Security user’s authorities and transforms them to a list of Group objects.

Based on this, we can also override the findGroupByQueryCriteria() and findGroupByQueryCriteriaCount() methods:

@Override
public List<Group> findGroupByQueryCriteria(GroupQueryImpl query, Page page) {
    if (query.getUserId() != null) {
        return findGroupsByUser(query.getUserId());
    }
    return null;
}

@Override
public long findGroupCountByQueryCriteria(GroupQueryImpl query) {
    return findGroupByQueryCriteria(query, null).size();
}

Other methods that update groups can be overridden to throw an exception:

public Group createNewGroup(String groupId) {
    throw new UnsupportedOperationException("This operation is not supported!");
}

4.3. Process Engine Configuration

After defining the two identity manager classes, we need to wire them into the configuration.

The spring starters auto-configure a SpringProcessEngineConfiguration for us. To modify this, we can use an InitializingBean:

@Autowired
private SpringProcessEngineConfiguration processEngineConfiguration;

@Autowired
private JdbcUserDetailsManager userManager;

@Bean
InitializingBean processEngineInitializer() {
    return new InitializingBean() {
        public void afterPropertiesSet() throws Exception {
            processEngineConfiguration.setUserEntityManager(
              new SpringSecurityUserManager(processEngineConfiguration,
              new MybatisUserDataManager(processEngineConfiguration), userManager));
            processEngineConfiguration.setGroupEntityManager(
              new SpringSecurityGroupManager(processEngineConfiguration,
              new MybatisGroupDataManager(processEngineConfiguration)));
            }
        };
    }

Here, the existing processEngineConfiguration is modified to use our custom identity managers.

If we want to set the current user in Activiti, we can use the method:

identityService.setAuthenticatedUserId(userId);

Keep in mind that this sets a ThreadLocal property, so the value is different for every thread.

5. Conclusion

In this article, we’ve seen the two ways we can integrate Activiti with Spring Security.

The full source code can be found over on GitHub.

Leave a Reply

Your email address will not be published.