diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflow.java b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflow.java index cf26ac59c35..c22d4f1bc69 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflow.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflow.java @@ -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 { diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/UserResourceTypeWorkflowProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserResourceTypeWorkflowProvider.java index 2f9766a3f1f..3b3acca5e9c 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/UserResourceTypeWorkflowProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserResourceTypeWorkflowProvider.java @@ -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); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionFactory.java deleted file mode 100644 index 8073d1e08fb..00000000000 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionFactory.java +++ /dev/null @@ -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 { - - 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() { - } - -} diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionProvider.java deleted file mode 100644 index 29802074cae..00000000000 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/ExpressionWorkflowConditionProvider.java +++ /dev/null @@ -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 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 - } -} diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EvaluatorUtils.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EvaluatorUtils.java index 373f9ac01d3..12eae7707f8 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EvaluatorUtils.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EvaluatorUtils.java @@ -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; + } } diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory index a9f7cb6ea63..b551bb564b7 100644 --- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory @@ -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 \ No newline at end of file +org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory \ No newline at end of file diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/DeleteUserWorkflowStepTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/DeleteUserWorkflowStepTest.java index 8564b436563..0ba62f5e080 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/DeleteUserWorkflowStepTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/DeleteUserWorkflowStepTest.java @@ -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 -> { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java index aebdee49732..764750cfe90 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java @@ -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-> {