Task Security in 7.0.1 - how to do it properly

Hello,

I would like to ensure that users of the rest api can only receive tasks that they have permission to see. I.e. if a user has an authority “sales”, then they will only see talks with an identitylink for the corresponding candidateGroup when accessing the REST API: /flowable-rest/process-api/runtime/tasks

I have got this working using AbstractCommandInterceptor but this seems too low level, and I guess there is a better way. Could someone point me how to do this “properly”.

Here is what I have:

ublic class CustomTaskCommandInterceptor extends AbstractCommandInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomTaskCommandInterceptor.class);
    private static final ThreadLocal<Boolean> isProcessing = ThreadLocal.withInitial(() -> false);

    @Override
    public <T> T execute(CommandConfig config, Command<T> command, CommandExecutor commandExecutor) {

        if (isProcessing.get()) {
            // to avoid recursion... :(
            return next.execute(config, command, commandExecutor);
        }

        try {
            isProcessing.set(true);
            LOGGER.info("Executing Task Command Interceptor");

            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null || !authentication.isAuthenticated()) {
                LOGGER.warn("User is not authenticated");
                return next.execute(config, command, commandExecutor);
            }

            String username = authentication.getName();
            List<String> userGroups = extractGroupsFromAuthorities(authentication.getAuthorities());

            if (isTaskQueryCommand(command)) {
                TaskQuery taskQuery = (TaskQuery) command;
                taskQuery.or()
                    .taskCandidateGroupIn(userGroups)
                    .taskAssignee(username)
                    .taskInvolvedUser(username)
                    .endOr();
                LOGGER.debug("Modified TaskQuery for user: {}", username);
            }

            return next.execute(config, command, commandExecutor);
        } finally {
            isProcessing.set(false);
        }
    }

    private List<String> extractGroupsFromAuthorities(Collection<? extends GrantedAuthority> authorities) {
        return authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
    }

    private boolean isTaskQueryCommand(Command<?> command) {
        String commandClassName = command.getClass().getSimpleName();
        return "TaskQueryImpl".equals(commandClassName) || "GetTasksCmd".equals(commandClassName) || "CountTasksCmd".equals(commandClassName);
    }
}

In my SecurityConfig I register the interceptor like so:


    @Bean
    public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> customProcessEngineConfigurer() {
        return processEngineConfiguration -> {
            logger.info("Configuring custom pre-command interceptors");
            ProcessEngineConfigurationImpl configImpl = (ProcessEngineConfigurationImpl) processEngineConfiguration;
            CommandInterceptor customInterceptor = new CustomTaskCommandInterceptor();

            if (configImpl.getCustomPreCommandInterceptors() == null) {
                configImpl.setCustomPreCommandInterceptors(new ArrayList<>());
            }

            configImpl.getCustomPreCommandInterceptors().add(customInterceptor);
        };
    }

It might be relevant to say that I am reading the user/groups from a JWT

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        logger.info("Configuring SecurityFilterChain");
        http
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(new LoggingGrantedAuthoritiesConverter());
        return converter;
    }

where the LoggingGrantedAuthoritiesConverter reads the JWT and adds roles to the grantedAuthorities.

Hey @tom,

I think the level you are solving this is too low. I would advise you to look into the different RestApiInterceptor(s) that we have.

Cheers,
Filip

Hi @filiphr - thanks for your reply

You mean something like:

public class CustomBpmnRestApiInterceptor implements BpmnRestApiInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomBpmnRestApiInterceptor.class);

    @Override
    public void accessTaskInfoWithQuery(TaskQuery taskQuery, TaskQueryRequest request) {
        LOGGER.info("Intercepting task query with custom permissions");

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            LOGGER.warn("User is not authenticated");
            return;
        }

        String username = authentication.getName();
        List<String> userGroups = extractGroupsFromAuthorities(authentication.getAuthorities());

        taskQuery.or()
                .taskCandidateGroupIn(userGroups)
                .taskAssignee(username)
                .taskInvolvedUser(username)
                .endOr();

        LOGGER.debug("TaskQuery modified for user: {}", username);
    }

    private List<String> extractGroupsFromAuthorities(Collection<? extends GrantedAuthority> authorities) {
        return authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
    }

And then registering with


    @Bean
    public BpmnRestApiInterceptor customBpmnRestApiInterceptor() {
        
        logger.info("Configuring CustomBpmnRestApiInterceptor");
        return new CustomBpmnRestApiInterceptor();
    }

The problem that I now have is that BpmnRestApiInterceptor is a very big interface to implement - there are like 80 methods. How can I make my life easier?

All the best,

Yes indeed just like that.

Well if you want to provide security over REST you’ll need to implement that interface. This is the cleanest approach if you ask me.