Skip to content

Commit

Permalink
build user task search endpoint that indexes tasks and allows to run …
Browse files Browse the repository at this point in the history
…queries for tasks assigned to users - supported for file system, mongodb and db
  • Loading branch information
mswiderski committed Jul 31, 2023
1 parent 60ec166 commit 3f6568c
Show file tree
Hide file tree
Showing 59 changed files with 2,784 additions and 78 deletions.
42 changes: 42 additions & 0 deletions addons/user-tasks/automatiko-user-tasks-index-db-addon/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.automatiko.addons</groupId>
<artifactId>user-tasks</artifactId>
<version>0.0.0-SNAPSHOT</version>
</parent>
<artifactId>automatiko-user-tasks-index-db-addon</artifactId>
<name>Automatiko Engine :: Add-Ons :: User Tasks :: Index :: DB</name>
<description>User task index based on DB AddOn for Automatiko Engine</description>
<properties>
<java.module.name>io.automatiko.addons.usertasks.index.db</java.module.name>
</properties>
<dependencies>
<dependency>
<groupId>io.automatiko.engine</groupId>
<artifactId>automatiko-engine-api</artifactId>
</dependency>
<dependency>
<groupId>io.automatiko.engine</groupId>
<artifactId>automatiko-engine-common</artifactId>
</dependency>
<dependency>
<groupId>io.automatiko.workflow</groupId>
<artifactId>automatiko-workflow-core</artifactId>
</dependency>
<dependency>
<groupId>io.automatiko.addons</groupId>
<artifactId>automatiko-user-tasks-index</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.automatiko.addons.usertasks.index.db;

import java.util.Collection;
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.automatiko.addon.usertasks.index.UserTask;
import io.automatiko.engine.api.event.DataEvent;
import io.automatiko.engine.api.event.EventPublisher;
import io.automatiko.engine.services.event.UserTaskInstanceDataEvent;
import io.automatiko.engine.services.event.impl.UserTaskInstanceEventBody;
import io.automatiko.engine.workflow.base.instance.impl.humantask.phases.Claim;
import io.automatiko.engine.workflow.base.instance.impl.humantask.phases.Release;
import io.automatiko.engine.workflow.base.instance.impl.workitem.Active;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class DBUserTaskEventPublisher implements EventPublisher {

private String serviceUrl;

private boolean keepCompleted;

@Inject
public DBUserTaskEventPublisher(
@ConfigProperty(name = "quarkus.automatiko.service-url") Optional<String> serviceUrl,
@ConfigProperty(name = "quarkus.automatiko.on-instance-end") Optional<String> onInstanceEnd) {
this.serviceUrl = serviceUrl.orElse("");
this.keepCompleted = onInstanceEnd.orElse("remove").equalsIgnoreCase("keep");
}

@Override
@Transactional
public void publish(DataEvent<?> event) {

if (event instanceof UserTaskInstanceDataEvent) {
UserTaskInstanceDataEvent uevent = (UserTaskInstanceDataEvent) event;
UserTaskInstanceEventBody data = uevent.getData();

UserTaskInfoEntity task = new UserTaskInfoEntity();

task.setId(data.getId());
task.setTaskName(data.getTaskName());
task.setTaskDescription(data.getTaskDescription());
task.setPotentialUsers(nullIfEmpty(data.getPotentialUsers()));
task.setPotentialGroups(nullIfEmpty(data.getPotentialGroups()));
task.setExcludedUsers(nullIfEmpty(data.getExcludedUsers()));
task.setTaskPriority(data.getTaskPriority());
task.setState(data.getState());
task.setActualOwner(data.getActualOwner());
task.setCompleteDate(data.getCompleteDate());
task.setFormLink(this.serviceUrl + data.getFormLink());
task.setProcessId(data.getProcessId());
task.setProcessInstanceId(data.getProcessInstanceId());
task.setRootProcessId(data.getRootProcessId());
task.setRootProcessInstanceId(data.getRootProcessInstanceId());
task.setReferenceId(data.getReferenceId());
task.setReferenceName(data.getReferenceName());
task.setStartDate(data.getStartDate());

if (keepCompleted || isActive(task)) {
UserTaskInfoEntity.persist(task);
} else {
UserTaskInfoEntity.deleteById(task.getId());
}
}

}

@Override
public void publish(Collection<DataEvent<?>> events) {
for (DataEvent<?> event : events) {
publish(event);
}
}

private Set<String> nullIfEmpty(Set<String> set) {
if (set == null || set.isEmpty()) {
return null;
}

return set;
}

private boolean isActive(UserTask task) {
return Active.STATUS.equalsIgnoreCase(task.getState()) || Claim.STATUS.equalsIgnoreCase(task.getState())
|| Release.STATUS.equalsIgnoreCase(task.getState());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package io.automatiko.addons.usertasks.index.db;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.automatiko.addon.usertasks.index.UserTask;
import io.automatiko.addon.usertasks.index.UserTaskIndexResource;
import io.automatiko.engine.api.auth.IdentityProvider;
import io.automatiko.engine.api.auth.IdentitySupplier;
import io.automatiko.engine.api.workflow.workitem.NotAuthorizedException;
import io.quarkus.arc.All;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.UriInfo;

@ApplicationScoped
public class DBUserTaskIndexResource implements UserTaskIndexResource {

private IdentitySupplier identitySupplier;

private Map<String, DbCustomQueryBuilder> customQueries = new HashMap<>();

@Inject
public DBUserTaskIndexResource(IdentitySupplier identitySupplier,
@All List<DbCustomQueryBuilder> queries) {
this.identitySupplier = identitySupplier;

queries.stream().forEach(q -> customQueries.put(q.id(), q));
}

@Override
public Collection<? extends UserTask> findTasks(String name, String description, String state,
String priority, int page, int size, String sortBy, boolean sortAsc, String user, List<String> groups) {

IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups);
try {
Sort sort = null;
if (sortBy != null) {
sort = Sort.by(sortBy(sortBy), sortAsc ? Sort.Direction.Ascending : Sort.Direction.Descending);
}
Map<String, Object> parameters = new HashMap<>();
StringBuilder builder = new StringBuilder(authFilter(identityProvider, parameters));

if (name != null) {
builder.append(" t.taskName like :taskName and");
parameters.put("taskName", "%" + name + " %");
}
if (description != null) {
builder.append(" t.taskDescription like :taskDescription and");
parameters.put("taskDescription", "%" + description + " %");
}
if (state != null) {
builder.append(" t.state = :state and");
parameters.put("state", state);
}
if (priority != null) {
builder.append(" t.taskPriority like :taskPriority and");
parameters.put("taskPriority", priority);
}

String query = builder.toString();
// remove the last and
query = query.substring(0, query.length() - 4);

return UserTaskInfoEntity.find(query, sort, parameters).page(calculatePage(page, size), size).list();
} finally {
IdentityProvider.set(null);
}
}

@Override
public UserTask findTask(String id, String user, List<String> groups) {
IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups);
try {

UserTaskInfoEntity entity = UserTaskInfoEntity.findById(id);

try {
enforceAuthorization(entity, identityProvider);
return entity;
} catch (NotAuthorizedException e) {
return null;
}

} finally {
IdentityProvider.set(null);
}
}

@Override
public Collection<? extends UserTask> queryTasks(UriInfo uriInfo, String name, int page, int size, String sortBy,
boolean sortAsc, String user, List<String> groups) {

DbCustomQueryBuilder customQuery = customQueries.get(name);

if (customQuery == null) {
throw new NotFoundException("Query with id '" + name + "' was not registered");
}

IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups);
try {
Sort sort = null;
if (sortBy != null) {
sort = Sort.by(sortBy(sortBy), sortAsc ? Sort.Direction.Ascending : Sort.Direction.Descending);
}
Map<String, Object> parameters = new HashMap<>();
StringBuilder builder = new StringBuilder(authFilter(identityProvider, parameters));

DbQueryFilter extraFilter = customQuery.build(uriInfo.getQueryParameters());

String query = builder.append(" " + extraFilter.queryFilter()).toString();
parameters.putAll(extraFilter.parameters());

return UserTaskInfoEntity.find(query, sort, parameters).page(calculatePage(page, size), size).list();
} finally {
IdentityProvider.set(null);
}
}

protected String authFilter(IdentityProvider identityProvider, Map<String, Object> parameters) {

parameters.put("user", identityProvider.getName());
parameters.put("groups", identityProvider.getRoles());

String authFilter = "from UserTaskInfoEntity t left join t.potentialUsers pu left join t.potentialGroups pg where (:user not member of t.excludedUsers) and (:user member of t.potentialUsers or pg in (:groups) or t.actualOwner = :user or (size(pg) < 1 and size(pu) < 1)) and ";

return authFilter;

}

protected void enforceAuthorization(UserTaskInfoEntity entity, IdentityProvider identity) {

if (identity != null) {
// in case identity/auth info is given enforce security restrictions
String user = identity.getName();
String currentOwner = entity.getActualOwner();
// if actual owner is already set always enforce same user
if (currentOwner != null && !currentOwner.trim().isEmpty() && !user.equals(currentOwner)) {

throw new NotAuthorizedException(
"User " + user + " is not authorized to access task instance with id " + entity.getId());
}

checkAssignedOwners(entity, user, identity);
}
}

protected void checkAssignedOwners(UserTaskInfoEntity entity, String user, IdentityProvider identity) {
// is not in the excluded users
if (entity.getExcludedUsers().contains(user)) {
throw new NotAuthorizedException(
"User " + user + " is not authorized to access task instance with id " + entity.getId());
}

// if there are no assignments means open to everyone
if (entity.getPotentialUsers().isEmpty() && entity.getPotentialGroups().isEmpty()) {
return;
}
// check if user is in potential users or groups
if (!entity.getPotentialUsers().contains(user) && entity.getPotentialGroups().stream().noneMatch(identity::hasRole)) {
throw new NotAuthorizedException(
"User " + user + " is not authorized to access task instance with id " + entity.getId());
}
}

protected int calculatePage(int page, int size) {
if (page <= 1) {
return 0;
}

return (page - 1) * size;
}

protected String sortBy(String sortBy) {

String fieldName = sortBy;

switch (sortBy) {
case "name":
fieldName = "taskName";
break;
case "description":
fieldName = "taskDescription";
break;
case "priority":
fieldName = "taskPriority";
break;
default:
break;
}

return "t." + fieldName;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.automatiko.addons.usertasks.index.db;

import io.automatiko.addon.usertasks.index.CustomQueryBuilder;

public abstract class DbCustomQueryBuilder implements CustomQueryBuilder<DbQueryFilter> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.automatiko.addons.usertasks.index.db;

import java.util.Map;

public class DbQueryFilter {

private final String queryFilter;

private final Map<String, Object> parameters;

public DbQueryFilter(String queryFilter, Map<String, Object> parameters) {
this.queryFilter = queryFilter;
this.parameters = parameters;
}

public String queryFilter() {
return queryFilter;
}

public Map<String, Object> parameters() {
return parameters;
}

}
Loading

0 comments on commit 3f6568c

Please sign in to comment.