Cache expression EvaluatorContext in the workflow component model's notes

Closes #42961

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen
2025-11-24 23:11:42 -03:00
committed by Pedro Igor
parent 5ae0e0a645
commit a2562caa11
8 changed files with 49 additions and 107 deletions

View File

@@ -2,8 +2,8 @@ package org.keycloak.models.workflow;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.workflow.conditions.ExpressionWorkflowConditionProvider;
import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser;
import org.keycloak.models.workflow.conditions.expression.ConditionEvaluator;
import org.keycloak.models.workflow.conditions.expression.EvaluatorUtils;
import org.keycloak.models.workflow.conditions.expression.EventEvaluator;
import org.keycloak.utils.StringUtil;
@@ -30,11 +30,10 @@ final class EventBasedWorkflow {
* Evaluates the specified context to determine whether the workflow should be activated or not. Activation will happen
* if the context's event matches the configured activation events and the resource conditions evaluate to true.
*
* @param executionContext
* @return
* @throws WorkflowInvalidStateException
* @param executionContext a reference to the workflow execution context.
* @return {@code true} if the workflow should be activated, {@code false} otherwise.
*/
boolean activate(WorkflowExecutionContext executionContext) throws WorkflowInvalidStateException {
boolean activate(WorkflowExecutionContext executionContext) {
WorkflowEvent event = executionContext.getEvent();
if (event == null) {
return false;
@@ -42,12 +41,12 @@ final class EventBasedWorkflow {
return supports(event.getResourceType()) && activateOnEvent(event) && validateResourceConditions(executionContext);
}
boolean deactivate(WorkflowExecutionContext executionContext) throws WorkflowInvalidStateException {
boolean deactivate(WorkflowExecutionContext executionContext) {
// TODO: rework this once we support concurrency/restart-if-running and concurrency/cancel-if-running to use expressions just like activation conditions
return false;
}
boolean restart(WorkflowExecutionContext executionContext) throws WorkflowInvalidStateException {
boolean restart(WorkflowExecutionContext executionContext) {
WorkflowEvent event = executionContext.getEvent();
if (event == null) {
return false;
@@ -57,14 +56,17 @@ final class EventBasedWorkflow {
public boolean validateResourceConditions(WorkflowExecutionContext context) {
String conditions = getModel().getConfig().getFirst(CONFIG_CONDITIONS);
if (StringUtil.isBlank(conditions)) {
if (StringUtil.isNotBlank(conditions)) {
BooleanConditionParser.EvaluatorContext evaluatorContext = EvaluatorUtils.createEvaluatorContext(model, conditions);
ConditionEvaluator evaluator = new ConditionEvaluator(session, context);
return evaluator.visit(evaluatorContext);
} else {
return true;
}
return new ExpressionWorkflowConditionProvider(getSession(), conditions).evaluate(context);
}
/**
* Determins whether the workflow should be activated based on the given event or not.
* Determines whether the workflow should be activated based on the given event or not.
*
* @param event a reference to the workflow event.
* @return {@code true} if the workflow should be activated, {@code false} otherwise.
@@ -77,7 +79,7 @@ final class EventBasedWorkflow {
String eventConditions = model.getConfig().getFirst(CONFIG_ON_EVENT);
if (StringUtil.isNotBlank(eventConditions)) {
BooleanConditionParser.EvaluatorContext context = EvaluatorUtils.createEvaluatorContext(eventConditions);
BooleanConditionParser.EvaluatorContext context = EvaluatorUtils.createEvaluatorContext(model, eventConditions);
EventEvaluator eventEvaluator = new EventEvaluator(getSession(), event);
return eventEvaluator.visit(context);
} else {

View File

@@ -32,7 +32,9 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.workflow.conditions.ExpressionWorkflowConditionProvider;
import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser;
import org.keycloak.models.workflow.conditions.expression.EvaluatorUtils;
import org.keycloak.models.workflow.conditions.expression.PredicateEvaluator;
import org.keycloak.utils.StringUtil;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
@@ -89,6 +91,8 @@ public class UserResourceTypeWorkflowProvider implements ResourceTypeSelector {
return cb.conjunction();
}
return new ExpressionWorkflowConditionProvider(session, conditions).toPredicate(cb, query, path);
BooleanConditionParser.EvaluatorContext context = EvaluatorUtils.createEvaluatorContext(conditions);
PredicateEvaluator evaluator = new PredicateEvaluator(session, cb, query, path);
return evaluator.visit(context);
}
}

View File

@@ -1,36 +0,0 @@
package org.keycloak.models.workflow.conditions;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
public class ExpressionWorkflowConditionFactory implements WorkflowConditionProviderFactory<ExpressionWorkflowConditionProvider> {
public static final String ID = "expression";
@Override
public ExpressionWorkflowConditionProvider create(KeycloakSession session, String configParameter) {
if (configParameter == null) {
throw new IllegalArgumentException("Expected single configuration parameter (expression)");
}
return new ExpressionWorkflowConditionProvider(session, configParameter);
}
@Override
public String getId() {
return ID;
}
@Override
public void init(org.keycloak.Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}

View File

@@ -1,52 +0,0 @@
package org.keycloak.models.workflow.conditions;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.workflow.WorkflowConditionProvider;
import org.keycloak.models.workflow.WorkflowExecutionContext;
import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser.EvaluatorContext;
import org.keycloak.models.workflow.conditions.expression.ConditionEvaluator;
import org.keycloak.models.workflow.conditions.expression.EvaluatorUtils;
import org.keycloak.models.workflow.conditions.expression.PredicateEvaluator;
public class ExpressionWorkflowConditionProvider implements WorkflowConditionProvider {
private final String expression;
private final KeycloakSession session;
private EvaluatorContext evaluatorContext;
public ExpressionWorkflowConditionProvider(KeycloakSession session, String expression) {
this.session = session;
this.expression = expression;
}
@Override
public boolean evaluate(WorkflowExecutionContext context) {
validate();
ConditionEvaluator evaluator = new ConditionEvaluator(session, context);
return evaluator.visit(this.evaluatorContext);
}
@Override
public Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> userRoot) {
validate();
PredicateEvaluator evaluator = new PredicateEvaluator(session, cb, query, userRoot);
return evaluator.visit(this.evaluatorContext);
}
@Override
public void validate() {
if (this.evaluatorContext == null) {
this.evaluatorContext = EvaluatorUtils.createEvaluatorContext(expression);
}
}
@Override
public void close() {
// no-op, nothing to close
}
}

View File

@@ -2,7 +2,9 @@ package org.keycloak.models.workflow.conditions.expression;
import java.util.stream.Collectors;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.workflow.WorkflowInvalidStateException;
import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser.EvaluatorContext;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
@@ -10,7 +12,14 @@ import org.antlr.v4.runtime.CommonTokenStream;
public class EvaluatorUtils {
public static BooleanConditionParser.EvaluatorContext createEvaluatorContext(String expression) {
/**
* Creates an EvaluatorContext from the given expression. If the expression is invalid, a WorkflowInvalidStateException
* is thrown with details about the parsing errors.
*
* @param expression the boolean expression to parse
* @return the EvaluatorContext representing the parsed expression
*/
public static EvaluatorContext createEvaluatorContext(String expression) {
// to properly validate the expression, we need to parse it.
CharStream charStream = CharStreams.fromString(expression);
BooleanConditionLexer lexer = new BooleanConditionLexer(charStream);
@@ -23,7 +32,7 @@ public class EvaluatorUtils {
parser.addErrorListener(errorListener);
// parse the expression and check for errors
BooleanConditionParser.EvaluatorContext context = parser.evaluator();
EvaluatorContext context = parser.evaluator();
if (errorListener.hasErrors()) {
String lineSeparator = System.lineSeparator();
String errorDetails = errorListener.getErrorMessages().stream()
@@ -35,4 +44,20 @@ public class EvaluatorUtils {
}
return context;
}
/**
* Creates or retrieves a cached EvaluatorContext for the given workflow model and expression.
*
* @param workflowModel the workflow component model
* @param expression the boolean expression to parse
* @return the EvaluatorContext representing the parsed expression
*/
public static EvaluatorContext createEvaluatorContext(ComponentModel workflowModel, String expression) {
EvaluatorContext context = workflowModel.getNote(expression);
if (context == null) {
context = createEvaluatorContext(expression);
workflowModel.setNote(expression, context);
}
return context;
}
}

View File

@@ -18,5 +18,4 @@
org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory
org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory
org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory
org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory
org.keycloak.models.workflow.conditions.ExpressionWorkflowConditionFactory
org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory

View File

@@ -199,7 +199,7 @@ public class DeleteUserWorkflowStepTest extends AbstractWorkflowTest {
oauth.openLoginForm();
loginPage.fillLogin(USER_NAME, USER_PASSWORD);
loginPage.submit();
assertTrue(driver.getPageSource().contains("Happy days"), "Test user should be successfully logged in.");
assertTrue(driver.driver().getPageSource().contains("Happy days"), "Test user should be successfully logged in.");
// check that we have two scheduled steps for the user
runOnServer.run((RunOnServer) session -> {

View File

@@ -92,7 +92,7 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
String username = userAlice.getUsername();
loginPage.fillLogin(username, userAlice.getPassword());
loginPage.submit();
assertTrue(driver.getPageSource() != null && driver.getPageSource().contains("Happy days"));
assertTrue(driver.page().getPageSource() != null && driver.page().getPageSource().contains("Happy days"));
// store the first step id for later comparison
String firstStepId = runOnServer.fetch(session-> {