diff --git a/core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java b/core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java deleted file mode 100644 index 992b7c389f4..00000000000 --- a/core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.keycloak.representations.workflows; - -import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_USES; -import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WITH; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.keycloak.common.util.MultivaluedHashMap; - -@JsonPropertyOrder({CONFIG_USES, CONFIG_WITH}) -public final class WorkflowConditionRepresentation extends AbstractWorkflowComponentRepresentation { - - public static Builder create() { - return new Builder(); - } - - public WorkflowConditionRepresentation() { - super(null, null, null); - } - - public WorkflowConditionRepresentation(String condition) { - this(condition, null); - } - - public WorkflowConditionRepresentation(String condition, MultivaluedHashMap config) { - super(null, condition, config); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof WorkflowConditionRepresentation)) { - return false; - } - WorkflowConditionRepresentation that = (WorkflowConditionRepresentation) obj; - return Objects.equals(getUses(), that.getUses()) && Objects.equals(getConfig(), that.getConfig()); - } - - @Override - @JsonSerialize(using = MultivaluedHashMapValueSerializer.class) - @JsonDeserialize(using = MultivaluedHashMapValueDeserializer.class) - public MultivaluedHashMap getConfig() { - return super.getConfig(); - } - - public static class Builder { - - private WorkflowConditionRepresentation action; - - public Builder of(String providerId) { - this.action = new WorkflowConditionRepresentation(providerId); - return this; - } - - public Builder withConfig(String key, String value) { - action.setConfig(key, value); - return this; - } - - public Builder withConfig(String key, String... values) { - action.setConfig(key, Arrays.asList(values)); - return this; - } - - public Builder withConfig(Map> config) { - action.setConfig(new MultivaluedHashMap<>(config)); - return this; - } - - public WorkflowConditionRepresentation build() { - return action; - } - } -} diff --git a/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java b/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java index 369a9866016..b7c0f13f749 100644 --- a/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java @@ -1,7 +1,7 @@ package org.keycloak.representations.workflows; -import static java.util.Optional.ofNullable; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONCURRENCY; +import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ENABLED; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_IF; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_NAME; @@ -13,14 +13,12 @@ import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_US import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -37,9 +35,6 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre private List steps; - @JsonProperty(CONFIG_IF) - private List conditions; - private WorkflowStateRepresentation state; @JsonProperty(CONFIG_CONCURRENCY) @@ -49,28 +44,17 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre super(null, null, null); } - public WorkflowRepresentation(String id, String workflow, MultivaluedHashMap config, List conditions, List steps) { + public WorkflowRepresentation(String id, String workflow, MultivaluedHashMap config, List steps) { super(id, workflow, config); - this.conditions = conditions; this.steps = steps; } - public T getOn() { - return getConfigValuesOrSingle(CONFIG_ON_EVENT); + public String getOn() { + return getConfigValue(CONFIG_ON_EVENT, String.class); } - public void setOn(String... events) { - setConfigValue(CONFIG_ON_EVENT, Arrays.asList(events)); - } - - @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - public void setOn(List events) { - setConfigValue(CONFIG_ON_EVENT, events); - } - - @JsonIgnore - public List getOnValues() { - return ofNullable(getConfigValues(CONFIG_ON_EVENT)).orElse(Collections.emptyList()); + public void setOn(String eventConditions) { + setConfigValue(CONFIG_ON_EVENT, eventConditions); } public String getName() { @@ -89,12 +73,13 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre setConfigValue(CONFIG_ENABLED, enabled); } - public void setConditions(List conditions) { - this.conditions = conditions; + @JsonProperty(CONFIG_IF) + public String getConditions() { + return getConfigValue(CONFIG_CONDITIONS, String.class); } - public List getConditions() { - return conditions; + public void setConditions(String conditions) { + setConfigValue(CONFIG_CONDITIONS, conditions); } public void setSteps(List steps) { @@ -145,7 +130,7 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre WorkflowRepresentation that = (WorkflowRepresentation) obj; // TODO: include state in comparison? return Objects.equals(getUses(), that.getUses()) && Objects.equals(getConfig(), that.getConfig()) - && Objects.equals(getConditions(), that.getConditions()) && Objects.equals(getSteps(), that.getSteps()); + && Objects.equals(getSteps(), that.getSteps()); } public static class Builder { @@ -175,8 +160,8 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre return this; } - public Builder onConditions(WorkflowConditionRepresentation... condition) { - representation.setConditions(Arrays.asList(condition)); + public Builder onCondition(String condition) { + representation.setConditions(condition); return this; } diff --git a/core/src/test/java/org/keycloak/representations/workflows/WorkflowDefinitionTest.java b/core/src/test/java/org/keycloak/representations/workflows/WorkflowDefinitionTest.java index fed94993966..57ef2a99a9a 100644 --- a/core/src/test/java/org/keycloak/representations/workflows/WorkflowDefinitionTest.java +++ b/core/src/test/java/org/keycloak/representations/workflows/WorkflowDefinitionTest.java @@ -2,7 +2,6 @@ package org.keycloak.representations.workflows; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import java.io.IOException; import java.time.Duration; @@ -24,29 +23,12 @@ public class WorkflowDefinitionTest { expected.setUses("my-provider"); expected.setName("my-name"); expected.setOn("event"); + expected.setConditions("condition-1(v1) AND (condition-2(key1:v1) OR condition-3(key2:v2,v3))"); expected.setSteps(null); - expected.setConditions(null); expected.setEnabled(true); expected.setConcurrency(new WorkflowConcurrencyRepresentation(true)); - expected.setConditions(Arrays.asList( - WorkflowConditionRepresentation.create() - .of("condition-1") - .withConfig("key1", "v1") - .withConfig("key2", "v1", "v2") - .build(), - WorkflowConditionRepresentation.create() - .of("condition-2") - .withConfig("key1", "v1") - .withConfig("key2", "v1", "v2") - .build(), - WorkflowConditionRepresentation.create() - .of("condition-1") - .withConfig("key1", "v1") - .withConfig("key2", "v1", "v2", "v3") - .build())); - expected.setSteps(Arrays.asList( WorkflowStepRepresentation.create() .of("step-1") @@ -71,26 +53,12 @@ public class WorkflowDefinitionTest { assertEquals(expected.getId(), actual.getId()); assertEquals(expected.getUses(), actual.getUses()); - assertTrue(actual.getOn() instanceof String); - assertEquals(expected.getOn(), (String) actual.getOn()); + assertNotNull(actual.getOn()); + assertEquals(expected.getOn(), actual.getOn()); assertEquals(expected.getConcurrency(), actual.getConcurrency()); assertEquals(expected.getName(), actual.getName()); assertEquals(expected.getEnabled(), actual.getEnabled()); - - List actualConditions = actual.getConditions(); - assertNotNull(actualConditions); - actualConditions = actualConditions.stream().sorted(Comparator.comparing(WorkflowConditionRepresentation::getUses)).collect(Collectors.toList()); - List expectedConditions = expected.getConditions().stream().sorted(Comparator.comparing(WorkflowConditionRepresentation::getUses)).collect(Collectors.toList()); - - assertEquals(expectedConditions.size(), actualConditions.size()); - assertEquals(expectedConditions.get(0).getUses(), actualConditions.get(0).getUses()); - assertEquals(expectedConditions.get(0).getConfig().get("key1"), actualConditions.get(0).getConfig().get("key1")); - assertEquals(expectedConditions.get(0).getConfig().get("key2"), actualConditions.get(0).getConfig().get("key2")); - assertEquals(expectedConditions.get(1).getConfig().get("key1"), actualConditions.get(1).getConfig().get("key1")); - assertEquals(expectedConditions.get(1).getConfig().get("key2"), actualConditions.get(1).getConfig().get("key2")); - assertEquals(expectedConditions.get(2).getConfig().get("key1"), actualConditions.get(2).getConfig().get("key1")); - assertEquals(expectedConditions.get(2).getConfig().get("key2"), actualConditions.get(2).getConfig().get("key2")); - + assertEquals(expected.getConditions(), actual.getConditions()); List actualSteps = actual.getSteps(); assertNotNull(actualSteps); @@ -106,30 +74,16 @@ public class WorkflowDefinitionTest { System.out.println(json); } - @Test - public void testOnEventAsArray() throws IOException { - WorkflowRepresentation expected = new WorkflowRepresentation(); - - expected.setOn("event", "event2"); - - String json = JsonSerialization.writeValueAsPrettyString(expected); - WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class); - assertTrue(actual.getOn() instanceof List); - assertEquals(Arrays.asList("event", "event2"), actual.getOn()); - - System.out.println(json); - } - @Test public void testOnEventAsString() throws IOException { WorkflowRepresentation expected = new WorkflowRepresentation(); - expected.setOn("event"); + expected.setOn("event OR other-event"); String json = JsonSerialization.writeValueAsPrettyString(expected); WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class); - assertTrue(actual.getOn() instanceof String); - assertEquals("event", actual.getOn()); + assertNotNull(actual.getOn()); + assertEquals("event OR other-event", actual.getOn()); System.out.println(json); } diff --git a/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanCondition.g4 b/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanCondition.g4 deleted file mode 100644 index e9cd575e2b3..00000000000 --- a/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanCondition.g4 +++ /dev/null @@ -1,30 +0,0 @@ -grammar BooleanCondition; - -// Parser Rules -evaluator : expression EOF; - -expression : expression OR andExpression | andExpression; -andExpression : andExpression AND notExpression | notExpression; -notExpression : '!' notExpression | atom; - -atom : LPAREN expression RPAREN - | conditionCall - ; - -conditionCall : Identifier LPAREN parameterList? RPAREN ; -parameterList : StringLiteral (COMMA StringLiteral)* ; - -// Lexer Rules -OR : 'OR'; -AND : 'AND'; -NOT : '!'; - -Identifier : [a-zA-Z_][a-zA-Z_0-9-]*; -StringLiteral : '"' ( ~'"' | '""' )* '"' ; - -// Explicitly defined tokens for the characters -LPAREN : '('; -RPAREN : ')'; -COMMA : ','; - -WS : [ \t\r\n]+ -> skip; \ No newline at end of file diff --git a/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionLexer.g4 b/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionLexer.g4 new file mode 100644 index 00000000000..51b31462445 --- /dev/null +++ b/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionLexer.g4 @@ -0,0 +1,22 @@ +lexer grammar BooleanConditionLexer; + +// --- DEFAULT_MODE (mode 0) --- +OR : 'OR'; +AND : 'AND'; +NOT : '!'; + +Identifier : [\p{L}_][\p{L}0-9_/-]*; + +LPAREN : '(' ; // For (A OR B) expressions +RPAREN : ')' ; // For (A OR B) expressions + +WS : [ \t\r\n]+ -> skip; + +// --- PARAM_MODE (mode 1) --- +mode PARAM_MODE; + + // 1. Matches the closing ')' and pops the mode + RPAREN_PARAM : ')' -> type(RPAREN), popMode ; + + // 2. This rule understands escape characters + ParameterText : ( '\\' . | ~[\\)] )+ ; \ No newline at end of file diff --git a/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionParser.g4 b/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionParser.g4 new file mode 100644 index 00000000000..d32e7ee8229 --- /dev/null +++ b/model/jpa/src/main/antlr4/org/keycloak/models/workflow/conditions/expression/BooleanConditionParser.g4 @@ -0,0 +1,28 @@ +parser grammar BooleanConditionParser; + +options { tokenVocab = BooleanConditionLexer; } + +// Parser Rules +evaluator : expression EOF; + +expression : expression OR andExpression | andExpression; + +andExpression : andExpression AND notExpression | notExpression; + +notExpression : '!' notExpression | atom; + +atom : LPAREN expression RPAREN // For grouping: (A OR B) + | conditionCall + ; + +conditionCall + // The 'pushMode' command must reference the Lexer name + : Identifier + { ((Lexer)_input.getTokenSource()).pushMode(BooleanConditionLexer.PARAM_MODE); } + LPAREN + parameter + RPAREN + | Identifier // The form without parentheses + ; + +parameter : ParameterText? ; \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java index 9ae69b86f5d..9741e9381b7 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java @@ -34,6 +34,8 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.jpa.entities.UserEntity; +import org.keycloak.models.workflow.conditions.ExpressionWorkflowConditionProvider; +import org.keycloak.utils.StringUtil; public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowProvider { @@ -65,33 +67,22 @@ public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowPro Predicate notExistsPredicate = cb.not(cb.exists(subquery)); predicates.add(notExistsPredicate); - predicates.addAll(getConditionsPredicate(cb, query, userRoot)); + predicates.add(getConditionsPredicate(cb, query, userRoot)); query.select(userRoot.get("id")).where(predicates); return em.createQuery(query).getResultList(); } - private List getConditionsPredicate(CriteriaBuilder cb, CriteriaQuery query, Root path) { + private Predicate getConditionsPredicate(CriteriaBuilder cb, CriteriaQuery query, Root path) { MultivaluedHashMap config = getModel().getConfig(); - List conditions = config.getOrDefault(CONFIG_CONDITIONS, List.of()); + String conditions = config.getFirst(CONFIG_CONDITIONS); - if (conditions.isEmpty()) { - return List.of(); + if (StringUtil.isBlank(conditions)) { + return cb.conjunction(); } - List predicates = new ArrayList<>(); - - for (String providerId : conditions) { - WorkflowConditionProvider condition = getManager().getConditionProvider(providerId, config); - Predicate predicate = condition.toPredicate(cb, query, path); - - if (predicate != null) { - predicates.add(predicate); - } - } - - return predicates; + return new ExpressionWorkflowConditionProvider(getSession(), conditions).toPredicate(cb, query, path); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java index 8dbef6a0d81..6453cd22fc8 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java @@ -8,17 +8,20 @@ import java.util.List; 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.EvaluatorUtils; +import org.keycloak.models.workflow.conditions.expression.EventEvaluator; +import org.keycloak.utils.StringUtil; public class EventBasedWorkflowProvider implements WorkflowProvider { private final KeycloakSession session; private final ComponentModel model; - private final WorkflowsManager manager; public EventBasedWorkflowProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; - this.manager = new WorkflowsManager(session); } @Override @@ -36,16 +39,12 @@ public class EventBasedWorkflowProvider implements WorkflowProvider { if (!supports(event.getResourceType())) { return false; } - - if (!isActivationEvent(event)) { - return false; - } - - return evaluate(event); + return isActivationEvent(event) && evaluateConditions(event); } @Override public boolean deactivateOnEvent(WorkflowEvent event) { + // TODO: rework this once we support concurrency/restart-if-running and concurrency/cancel-if-running to use expressions just like activation conditions if (!supports(event.getResourceType())) { return false; } @@ -56,7 +55,7 @@ public class EventBasedWorkflowProvider implements WorkflowProvider { ResourceOperationType a = ResourceOperationType.valueOf(activationEvent); if (a.isDeactivationEvent(event.getEvent().getClass())) { - return !evaluate(event); + return !evaluateConditions(event); } } @@ -65,38 +64,35 @@ public class EventBasedWorkflowProvider implements WorkflowProvider { @Override public boolean resetOnEvent(WorkflowEvent event) { - return isCancelIfRunning() && evaluate(event); + return isCancelIfRunning() && evaluateConditions(event); } @Override public void close() { - } - protected boolean evaluate(WorkflowEvent event) { - List conditions = getModel().getConfig().getOrDefault(CONFIG_CONDITIONS, List.of()); - - for (String providerId : conditions) { - WorkflowConditionProvider condition = manager.getConditionProvider(providerId, model.getConfig()); - - if (!condition.evaluate(event)) { - return false; - } + protected boolean evaluateConditions(WorkflowEvent event) { + String conditions = getModel().getConfig().getFirst(CONFIG_CONDITIONS); + if (StringUtil.isBlank(conditions)) { + return true; } - - return true; + return new ExpressionWorkflowConditionProvider(getSession(), conditions).evaluate(event); } protected boolean isActivationEvent(WorkflowEvent event) { - ResourceOperationType operation = event.getOperation(); - - if (ResourceOperationType.AD_HOC.equals(operation)) { + // AD_HOC is a special case that always triggers the workflow regardless of the configured activation events + if (ResourceOperationType.AD_HOC.equals(event.getOperation())) { return true; } - List events = model.getConfig().getOrDefault(CONFIG_ON_EVENT, List.of()); - - return events.contains(operation.name()); + String eventConditions = model.getConfig().getFirst(CONFIG_ON_EVENT); + if (StringUtil.isNotBlank(eventConditions)) { + BooleanConditionParser.EvaluatorContext context = EvaluatorUtils.createEvaluatorContext(eventConditions); + EventEvaluator eventEvaluator = new EventEvaluator(getSession(), event); + return eventEvaluator.visit(context); + } else { + return false; + } } protected ComponentModel getModel() { @@ -107,10 +103,6 @@ public class EventBasedWorkflowProvider implements WorkflowProvider { return session; } - protected WorkflowsManager getManager() { - return manager; - } - protected boolean isCancelIfRunning() { return Boolean.parseBoolean(model.getConfig().getFirstOrDefault(CONFIG_CANCEL_IF_RUNNING, "false")); } diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java index 9269627e0aa..3f496113b2a 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java @@ -20,7 +20,7 @@ package org.keycloak.models.workflow; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; -import static org.keycloak.models.workflow.ResourceOperationType.USER_ADD; +import static org.keycloak.models.workflow.ResourceOperationType.USER_ADDED; public class UserCreationTimeWorkflowProvider extends AbstractUserWorkflowProvider { @@ -30,6 +30,6 @@ public class UserCreationTimeWorkflowProvider extends AbstractUserWorkflowProvid @Override protected boolean isActivationEvent(WorkflowEvent event) { - return super.isActivationEvent(event) || USER_ADD.equals(event.getOperation()); + return super.isActivationEvent(event) || USER_ADDED.equals(event.getOperation()); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java index 0c75584d15c..4b69fa11b97 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java @@ -17,8 +17,8 @@ package org.keycloak.models.workflow; -import static org.keycloak.models.workflow.ResourceOperationType.USER_ADD; -import static org.keycloak.models.workflow.ResourceOperationType.USER_LOGIN; +import static org.keycloak.models.workflow.ResourceOperationType.USER_ADDED; +import static org.keycloak.models.workflow.ResourceOperationType.USER_LOGGED_IN; import java.util.List; @@ -33,6 +33,6 @@ public class UserSessionRefreshTimeWorkflowProvider extends AbstractUserWorkflow @Override protected boolean isActivationEvent(WorkflowEvent event) { - return super.isActivationEvent(event) || List.of(USER_ADD, USER_LOGIN).contains(event.getOperation()); + return super.isActivationEvent(event) || List.of(USER_ADDED, USER_LOGGED_IN).contains(event.getOperation()); } } 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 index e252121dc72..8073d1e08fb 100644 --- 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 @@ -4,25 +4,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.workflow.WorkflowConditionProviderFactory; -import java.util.List; -import java.util.Map; - public class ExpressionWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "expression"; - public static final String EXPRESSION = "expression"; @Override - public ExpressionWorkflowConditionProvider create(KeycloakSession session, Map> config) { - return new ExpressionWorkflowConditionProvider(session, config.getOrDefault(EXPRESSION, List.of()).stream().findFirst().orElse("")); - } - - @Override - public ExpressionWorkflowConditionProvider create(KeycloakSession session, List configParameters) { - if (configParameters.size() > 1) { + public ExpressionWorkflowConditionProvider create(KeycloakSession session, String configParameter) { + if (configParameter == null) { throw new IllegalArgumentException("Expected single configuration parameter (expression)"); } - return create(session, Map.of(EXPRESSION, configParameters)); + return new ExpressionWorkflowConditionProvider(session, configParameter); } @Override 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 index bdd75e89c35..65ce86cba7f 100644 --- 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 @@ -4,21 +4,13 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.keycloak.models.KeycloakSession; import org.keycloak.models.workflow.WorkflowConditionProvider; import org.keycloak.models.workflow.WorkflowEvent; -import org.keycloak.models.workflow.WorkflowInvalidStateException; -import org.keycloak.models.workflow.conditions.expression.BooleanConditionEvaluator; -import org.keycloak.models.workflow.conditions.expression.BooleanConditionLexer; -import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser; +import org.keycloak.models.workflow.conditions.expression.ConditionEvaluator; import org.keycloak.models.workflow.conditions.expression.BooleanConditionParser.EvaluatorContext; -import org.keycloak.models.workflow.conditions.expression.ErrorListener; -import org.keycloak.models.workflow.conditions.expression.PredicateConditionEvaluator; - -import java.util.stream.Collectors; +import org.keycloak.models.workflow.conditions.expression.EvaluatorUtils; +import org.keycloak.models.workflow.conditions.expression.PredicateEvaluator; public class ExpressionWorkflowConditionProvider implements WorkflowConditionProvider { @@ -34,44 +26,21 @@ public class ExpressionWorkflowConditionProvider implements WorkflowConditionPro @Override public boolean evaluate(WorkflowEvent event) { validate(); - BooleanConditionEvaluator evaluator = new BooleanConditionEvaluator(session, event); + ConditionEvaluator evaluator = new ConditionEvaluator(session, event); return evaluator.visit(this.evaluatorContext); } @Override public Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery query, Root userRoot) { validate(); - PredicateConditionEvaluator evaluator = new PredicateConditionEvaluator(session, cb, query, userRoot); + PredicateEvaluator evaluator = new PredicateEvaluator(session, cb, query, userRoot); return evaluator.visit(this.evaluatorContext); } @Override public void validate() { - // to properly validate the expression, we need to parse it. We then cache the parsed context if the expression is valid - // so we don't to parse it again if validate is called again on the same instance of the provider if (this.evaluatorContext == null) { - CharStream charStream = CharStreams.fromString(expression); - BooleanConditionLexer lexer = new BooleanConditionLexer(charStream); - CommonTokenStream tokens = new CommonTokenStream(lexer); - BooleanConditionParser parser = new BooleanConditionParser(tokens); - - // this replaces the standard error listener, storing all parsing errors if the expressions is malformed - ErrorListener errorListener = new ErrorListener(); - parser.removeErrorListeners(); - parser.addErrorListener(errorListener); - - // parse the expression and check for errors - EvaluatorContext context = parser.evaluator(); - if (errorListener.hasErrors()) { - String lineSeparator = System.lineSeparator(); - String errorDetails = errorListener.getErrorMessages().stream() - .map(error -> "- " + error) - .collect(Collectors.joining(lineSeparator)); - - throw new WorkflowInvalidStateException(String.format("Invalid expression: %s%sError details:%s%s", - expression, lineSeparator, lineSeparator, errorDetails)); - } - this.evaluatorContext = context; + this.evaluatorContext = EvaluatorUtils.createEvaluatorContext(expression); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java index aceaa1e4f70..7b297ba6801 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java @@ -1,8 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; -import java.util.Map; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.workflow.WorkflowConditionProviderFactory; @@ -10,16 +7,10 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory; public class GroupMembershipWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "is-member-of"; - public static final String EXPECTED_GROUPS = "groups"; @Override - public GroupMembershipWorkflowConditionProvider create(KeycloakSession session, Map> config) { - return new GroupMembershipWorkflowConditionProvider(session, config.get(EXPECTED_GROUPS)); - } - - @Override - public GroupMembershipWorkflowConditionProvider create(KeycloakSession session, List configParameters) { - return new GroupMembershipWorkflowConditionProvider(session, configParameters); + public GroupMembershipWorkflowConditionProvider create(KeycloakSession session, String configParameter) { + return new GroupMembershipWorkflowConditionProvider(session, configParameter); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java index ef8a2631f03..13dbda4cd78 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java @@ -1,24 +1,24 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; - import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.workflow.WorkflowConditionProvider; import org.keycloak.models.workflow.WorkflowEvent; import org.keycloak.models.workflow.WorkflowInvalidStateException; import org.keycloak.models.workflow.ResourceType; +import org.keycloak.utils.StringUtil; public class GroupMembershipWorkflowConditionProvider implements WorkflowConditionProvider { - private final List expectedGroups; + private final String expectedGroup; private final KeycloakSession session; - public GroupMembershipWorkflowConditionProvider(KeycloakSession session, List expectedGroups) { + public GroupMembershipWorkflowConditionProvider(KeycloakSession session,String expectedGroup) { this.session = session; - this.expectedGroups = expectedGroups;; + this.expectedGroup = expectedGroup; } @Override @@ -32,25 +32,22 @@ public class GroupMembershipWorkflowConditionProvider implements WorkflowConditi String userId = event.getResourceId(); RealmModel realm = session.getContext().getRealm(); UserModel user = session.users().getUserById(realm, userId); - - for (String expectedGroup : expectedGroups) { - GroupModel group = session.groups().getGroupById(realm, expectedGroup); - - if (user.isMemberOf(group)) { - return true; - } + if (user == null) { + return false; } - return false; + GroupModel group = KeycloakModelUtils.findGroupByPath(session, realm, expectedGroup); + return user.isMemberOf(group); } @Override public void validate() { - expectedGroups.forEach(id -> { - if (session.groups().getGroupById(session.getContext().getRealm(), id) == null) { - throw new WorkflowInvalidStateException(String.format("Group with id %s does not exist.", id)); - } - }); + if (StringUtil.isBlank(this.expectedGroup)) { + throw new WorkflowInvalidStateException("Expected group path not set."); + } + if (KeycloakModelUtils.findGroupByPath(session, session.getContext().getRealm(), expectedGroup) == null) { + throw new WorkflowInvalidStateException(String.format("Group with name %s does not exist.", expectedGroup)); + } } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java index 7e633a92ffd..429233340e2 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java @@ -1,8 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; -import java.util.Map; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.workflow.WorkflowConditionProviderFactory; @@ -10,16 +7,10 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory; public class IdentityProviderWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "has-identity-provider-link"; - public static final String EXPECTED_ALIASES = "alias"; @Override - public IdentityProviderWorkflowConditionProvider create(KeycloakSession session, Map> config) { - return new IdentityProviderWorkflowConditionProvider(session, config.get(EXPECTED_ALIASES)); - } - - @Override - public IdentityProviderWorkflowConditionProvider create(KeycloakSession session, List configParameters) { - return new IdentityProviderWorkflowConditionProvider(session, configParameters); + public IdentityProviderWorkflowConditionProvider create(KeycloakSession session, String configParameter) { + return new IdentityProviderWorkflowConditionProvider(session, configParameter); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java index d556ef7adac..3720cfb6e50 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java @@ -1,6 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; import java.util.stream.Stream; import jakarta.persistence.criteria.CriteriaBuilder; @@ -17,15 +16,16 @@ import org.keycloak.models.workflow.WorkflowConditionProvider; import org.keycloak.models.workflow.WorkflowEvent; import org.keycloak.models.workflow.WorkflowInvalidStateException; import org.keycloak.models.workflow.ResourceType; +import org.keycloak.utils.StringUtil; public class IdentityProviderWorkflowConditionProvider implements WorkflowConditionProvider { - private final List expectedAliases; + private final String expectedAlias; private final KeycloakSession session; - public IdentityProviderWorkflowConditionProvider(KeycloakSession session, List expectedAliases) { + public IdentityProviderWorkflowConditionProvider(KeycloakSession session, String expectedAlias) { this.session = session; - this.expectedAliases = expectedAliases;; + this.expectedAlias = expectedAlias; } @Override @@ -39,11 +39,14 @@ public class IdentityProviderWorkflowConditionProvider implements WorkflowCondit String userId = event.getResourceId(); RealmModel realm = session.getContext().getRealm(); UserModel user = session.users().getUserById(realm, userId); - Stream federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user); + if (user == null) { + return false; + } + Stream federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user); return federatedIdentities .map(FederatedIdentityModel::getIdentityProvider) - .anyMatch(expectedAliases::contains); + .anyMatch(expectedAlias::equals); } @Override @@ -55,7 +58,7 @@ public class IdentityProviderWorkflowConditionProvider implements WorkflowCondit subquery.where( cb.and( cb.equal(from.get("user").get("id"), path.get("id")), - from.get("identityProvider").in(expectedAliases) + cb.equal(from.get("identityProvider"), expectedAlias) ) ); @@ -64,11 +67,12 @@ public class IdentityProviderWorkflowConditionProvider implements WorkflowCondit @Override public void validate() { - expectedAliases.forEach(alias -> { - if (session.identityProviders().getByAlias(alias) == null) { - throw new WorkflowInvalidStateException(String.format("Identity provider %s does not exist.", alias)); - } - }); + if (StringUtil.isBlank(expectedAlias)) { + throw new WorkflowInvalidStateException("Expected identity provider alias is not set."); + } + if (session.identityProviders().getByAlias(expectedAlias) == null) { + throw new WorkflowInvalidStateException(String.format("Identity provider %s does not exist.", expectedAlias)); + } } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java index 27c17d04372..af4ef03b424 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java @@ -1,8 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; -import java.util.Map; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.workflow.WorkflowConditionProviderFactory; @@ -10,16 +7,10 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory; public class RoleWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "has-role"; - public static final String EXPECTED_ROLES = "roles"; @Override - public RoleWorkflowConditionProvider create(KeycloakSession session, Map> config) { - return new RoleWorkflowConditionProvider(session, config.get(EXPECTED_ROLES)); - } - - @Override - public RoleWorkflowConditionProvider create(KeycloakSession session, List configParameters) { - return new RoleWorkflowConditionProvider(session, configParameters); + public RoleWorkflowConditionProvider create(KeycloakSession session, String expectedRole) { + return new RoleWorkflowConditionProvider(session, expectedRole); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java index ab5e0395b9d..f73161b1b35 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java @@ -1,6 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -14,15 +13,16 @@ import org.keycloak.models.workflow.WorkflowEvent; import org.keycloak.models.workflow.WorkflowInvalidStateException; import org.keycloak.models.workflow.ResourceType; import org.keycloak.models.utils.RoleUtils; +import org.keycloak.utils.StringUtil; public class RoleWorkflowConditionProvider implements WorkflowConditionProvider { - private final List expectedRoles; + private final String expectedRole; private final KeycloakSession session; - public RoleWorkflowConditionProvider(KeycloakSession session, List expectedRoles) { + public RoleWorkflowConditionProvider(KeycloakSession session, String expectedRole) { this.session = session; - this.expectedRoles = expectedRoles; + this.expectedRole = expectedRole; } @Override @@ -31,6 +31,8 @@ public class RoleWorkflowConditionProvider implements WorkflowConditionProvider return false; } + validate(); + String userId = event.getResourceId(); RealmModel realm = session.getContext().getRealm(); UserModel user = session.users().getUserById(realm, userId); @@ -40,25 +42,18 @@ public class RoleWorkflowConditionProvider implements WorkflowConditionProvider } Set roles = user.getRoleMappingsStream().collect(Collectors.toSet()); - - for (String name : expectedRoles) { - RoleModel expectedRole = getRole(name, realm); - - if (expectedRole == null || !RoleUtils.hasRole(roles, expectedRole)) { - return false; - } - } - - return true; + RoleModel role = getRole(expectedRole, realm); + return role != null && RoleUtils.hasRole(roles, role); } @Override public void validate() throws WorkflowInvalidStateException { - expectedRoles.forEach(id -> { - if (session.roles().getRoleById(session.getContext().getRealm(), id) == null) { - throw new WorkflowInvalidStateException(String.format("Role with id %s does not exist.", id)); - } - }); + if (StringUtil.isBlank(expectedRole)) { + throw new WorkflowInvalidStateException("Expected role name not set."); + } + if (getRole(expectedRole, session.getContext().getRealm()) == null) { + throw new WorkflowInvalidStateException(String.format("Role with name %s does not exist.", expectedRole)); + } } private RoleModel getRole(String expectedRole, RealmModel realm) { diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java index 302d4f7148d..a7e27ede327 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java @@ -1,8 +1,5 @@ package org.keycloak.models.workflow.conditions; -import java.util.List; -import java.util.Map; - import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.workflow.WorkflowConditionProviderFactory; @@ -12,25 +9,8 @@ public class UserAttributeWorkflowConditionFactory implements WorkflowConditionP public static final String ID = "has-user-attribute"; @Override - public UserAttributeWorkflowConditionProvider create(KeycloakSession session, Map> config) { - return new UserAttributeWorkflowConditionProvider(session, config); - } - - @Override - public UserAttributeWorkflowConditionProvider create(KeycloakSession session, List configParameters) { - if (configParameters.size() % 2 != 0) { - throw new IllegalArgumentException("Expected even number of configuration parameters (attribute key/value pairs)"); - } - // Convert list of parameters into map of expected attributes - Map> expectedAttributes = new java.util.HashMap<>(); - for (int i = 0; i < configParameters.size(); i += 2) { - String key = configParameters.get(i); - String value = configParameters.get(i + 1); - // value can have multiple values separated by comma - List values = List.of(value.split(",")); - expectedAttributes.put(key, values); - } - return create(session, expectedAttributes); + public UserAttributeWorkflowConditionProvider create(KeycloakSession session, String keyValuePair) { + return new UserAttributeWorkflowConditionProvider(session, keyValuePair); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java index 82daee9a340..633034abb84 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java @@ -2,9 +2,9 @@ package org.keycloak.models.workflow.conditions; import static org.keycloak.common.util.CollectionUtil.collectionEquals; +import java.io.StringReader; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; +import java.util.Properties; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -12,15 +12,16 @@ import org.keycloak.models.UserModel; import org.keycloak.models.workflow.WorkflowConditionProvider; import org.keycloak.models.workflow.WorkflowEvent; import org.keycloak.models.workflow.ResourceType; +import org.keycloak.models.workflow.WorkflowInvalidStateException; public class UserAttributeWorkflowConditionProvider implements WorkflowConditionProvider { - private final Map> expectedAttributes; + private final String expectedAttribute; private final KeycloakSession session; - public UserAttributeWorkflowConditionProvider(KeycloakSession session, Map> expectedAttributes) { + public UserAttributeWorkflowConditionProvider(KeycloakSession session, String expectedAttribute) { this.session = session; - this.expectedAttributes = expectedAttributes;; + this.expectedAttribute = expectedAttribute; } @Override @@ -29,6 +30,8 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition return false; } + validate(); + String userId = event.getResourceId(); RealmModel realm = session.getContext().getRealm(); UserModel user = session.users().getUserById(realm, userId); @@ -37,25 +40,41 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition return false; } - for (Entry> expected : expectedAttributes.entrySet()) { - List values = user.getAttributes().getOrDefault(expected.getKey(), List.of()); - List expectedValues = expected.getValue(); + String[] parsedKeyValuePair = parseKeyValuePair(expectedAttribute); + List values = user.getAttributes().getOrDefault(parsedKeyValuePair[0], List.of()); + List expectedValues = List.of(parsedKeyValuePair[1].split(",")); - if (!collectionEquals(expectedValues, values)) { - return false; - } - } - - return true; + return collectionEquals(expectedValues, values); } @Override public void validate() { - // no-op + if (expectedAttribute == null) { + throw new WorkflowInvalidStateException("Expected 'key:value' pair is not set."); + } } @Override public void close() { } + + /** + * Parses a key-value pair string in the format "key:value" and returns an array containing the key and value. It relies + * on Properties.load to handle edge cases like escaped colons. + * + * @param keyValuePair the key-value pair string to parse + * @return a {@link String} array where the first element is the key and the second element is the value. + */ + public static String[] parseKeyValuePair(String keyValuePair) { + Properties props = new Properties(); + try { + props.load(new StringReader(keyValuePair)); + } catch (java.io.IOException e) { + throw new WorkflowInvalidStateException("Error reading key-value pair " + keyValuePair + ". Expected format 'key:value'"); + } + String key = props.stringPropertyNames().iterator().next(); + String value = props.getProperty(key); + return new String[]{key, value}; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/BooleanConditionEvaluator.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/ConditionEvaluator.java similarity index 55% rename from model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/BooleanConditionEvaluator.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/ConditionEvaluator.java index 9a4ec79403e..f8c59250c76 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/BooleanConditionEvaluator.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/ConditionEvaluator.java @@ -1,25 +1,20 @@ package org.keycloak.models.workflow.conditions.expression; -import org.antlr.v4.runtime.tree.ParseTree; import org.keycloak.models.KeycloakSession; import org.keycloak.models.workflow.WorkflowConditionProvider; -import org.keycloak.models.workflow.WorkflowConditionProviderFactory; import org.keycloak.models.workflow.WorkflowEvent; import org.keycloak.models.workflow.WorkflowsManager; -import java.util.List; -import java.util.stream.Collectors; +public class ConditionEvaluator extends BooleanConditionParserBaseVisitor { -public class BooleanConditionEvaluator extends BooleanConditionBaseVisitor { + protected final KeycloakSession session; + protected final WorkflowEvent event; + protected final WorkflowsManager manager; - private final KeycloakSession session; - private final WorkflowEvent event; - private final WorkflowsManager manager; - - public BooleanConditionEvaluator(KeycloakSession session, WorkflowEvent event) { + public ConditionEvaluator(KeycloakSession session, WorkflowEvent event) { this.session = session; this.event = event; - this.manager = new WorkflowsManager(session); + this.manager = new WorkflowsManager(session); } @Override @@ -62,22 +57,32 @@ public class BooleanConditionEvaluator extends BooleanConditionBaseVisitor providerFactory = manager.getConditionProviderFactory(conditionName); - WorkflowConditionProvider conditionProvider = providerFactory.create(session, extractParameterList(ctx.parameterList())); + WorkflowConditionProvider conditionProvider = manager.getConditionProvider(conditionName, extractParameter(ctx.parameter())); return conditionProvider.evaluate(event); } - private List extractParameterList(BooleanConditionParser.ParameterListContext ctx) { - if (ctx == null) { - return List.of(); + protected String extractParameter(BooleanConditionParser.ParameterContext paramCtx) { + // Case 1: No parentheses were used (e.g., "user-logged-in") + // Case 2: Empty parentheses were used (e.g., "user-logged-in()") + if (paramCtx == null || paramCtx.ParameterText() == null) { + return null; } - return ctx.StringLiteral().stream() - .map(this::visitStringLiteral) - .collect(Collectors.toList()); + + // Case 3: A parameter was provided (e.g., "has-role(param)") + String rawText = paramCtx.ParameterText().getText(); + return unEscapeParameter(rawText); } - private String visitStringLiteral(ParseTree ctx) { - String text = ctx.getText(); - return text.substring(1, text.length() - 1).replace("\"\"", "\""); + /** + * The grammar defines escapes as '\)' and '\\'. + * @param rawText The raw text from the ParameterText token. + * @return A clean, un-escaped string. + */ + private String unEscapeParameter(String rawText) { + // This handles both \) -> ) and \\ -> \ + // Note: replaceAll uses regex, so we must double-escape the backslashes + return rawText.replace("\\)", ")") + .replace("\\\\", "\\"); } + } 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 new file mode 100644 index 00000000000..04763c34474 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EvaluatorUtils.java @@ -0,0 +1,37 @@ +package org.keycloak.models.workflow.conditions.expression; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.keycloak.models.workflow.WorkflowInvalidStateException; + +import java.util.stream.Collectors; + +public class EvaluatorUtils { + + public static BooleanConditionParser.EvaluatorContext createEvaluatorContext(String expression) { + // to properly validate the expression, we need to parse it. + CharStream charStream = CharStreams.fromString(expression); + BooleanConditionLexer lexer = new BooleanConditionLexer(charStream); + CommonTokenStream tokens = new CommonTokenStream(lexer); + BooleanConditionParser parser = new BooleanConditionParser(tokens); + + // this replaces the standard error listener, storing all parsing errors if the expressions is malformed + ErrorListener errorListener = new ErrorListener(); + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + + // parse the expression and check for errors + BooleanConditionParser.EvaluatorContext context = parser.evaluator(); + if (errorListener.hasErrors()) { + String lineSeparator = System.lineSeparator(); + String errorDetails = errorListener.getErrorMessages().stream() + .map(error -> "- " + error) + .collect(Collectors.joining(lineSeparator)); + + throw new WorkflowInvalidStateException(String.format("Invalid expression: %s%sError details:%s%s", + expression, lineSeparator, lineSeparator, errorDetails)); + } + return context; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EventEvaluator.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EventEvaluator.java new file mode 100644 index 00000000000..55551719b9a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/EventEvaluator.java @@ -0,0 +1,20 @@ +package org.keycloak.models.workflow.conditions.expression; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.WorkflowEvent; + +public class EventEvaluator extends ConditionEvaluator { + + public EventEvaluator(KeycloakSession session, WorkflowEvent event) { + super(session, event); + } + + @Override + public Boolean visitConditionCall(BooleanConditionParser.ConditionCallContext ctx) { + String name = ctx.Identifier().getText(); + ResourceOperationType operation = ResourceOperationType.valueOf(name.replace("-", "_").toUpperCase()); + String param = super.extractParameter(ctx.parameter()); + return operation.test(super.event, param); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateConditionEvaluator.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateEvaluator.java similarity index 60% rename from model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateConditionEvaluator.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateEvaluator.java index bcd6656ede8..b43d8770b94 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateConditionEvaluator.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/expression/PredicateEvaluator.java @@ -4,31 +4,29 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import org.antlr.v4.runtime.tree.ParseTree; import org.keycloak.models.KeycloakSession; import org.keycloak.models.workflow.WorkflowConditionProvider; -import org.keycloak.models.workflow.WorkflowConditionProviderFactory; import org.keycloak.models.workflow.WorkflowsManager; -import java.util.List; -import java.util.stream.Collectors; +public class PredicateEvaluator extends BooleanConditionParserBaseVisitor { -public class PredicateConditionEvaluator extends BooleanConditionBaseVisitor { - - private final KeycloakSession session; private final CriteriaBuilder cb; private final CriteriaQuery query; private final Root root; - private WorkflowsManager manager; + private final WorkflowsManager manager; - public PredicateConditionEvaluator(KeycloakSession session, CriteriaBuilder cb, CriteriaQuery query, Root root) { - this.session = session; + public PredicateEvaluator(KeycloakSession session, CriteriaBuilder cb, CriteriaQuery query, Root root) { this.cb = cb; this.query = query; this.root = root; this.manager = new WorkflowsManager(session); } + @Override + public Predicate visitEvaluator(BooleanConditionParser.EvaluatorContext ctx) { + return visit(ctx.expression()); + } + @Override public Predicate visitExpression(BooleanConditionParser.ExpressionContext ctx) { // Handle 'expression OR andExpression' @@ -55,7 +53,7 @@ public class PredicateConditionEvaluator extends BooleanConditionBaseVisitor
 providerFactory = manager.getConditionProviderFactory(conditionName);
-        WorkflowConditionProvider conditionProvider = providerFactory.create(session, extractParameterList(ctx.parameterList()));
+        WorkflowConditionProvider conditionProvider = manager.getConditionProvider(conditionName, extractParameter(ctx.parameter()));
         return conditionProvider.toPredicate(cb, query, root);
     }
 
-    private List extractParameterList(BooleanConditionParser.ParameterListContext ctx) {
-        if (ctx == null) {
-            return List.of();
+    protected String extractParameter(BooleanConditionParser.ParameterContext paramCtx) {
+        // Case 1: No parentheses were used (e.g., "user-logged-in")
+        // Case 2: Empty parentheses were used (e.g., "user-logged-in()")
+        if (paramCtx == null || paramCtx.ParameterText() == null) {
+            return null;
         }
-        return ctx.StringLiteral().stream()
-                .map(this::visitStringLiteral)
-                .collect(Collectors.toList());
+
+        // Case 3: A parameter was provided (e.g., "has-role(param)")
+        String rawText = paramCtx.ParameterText().getText();
+        return unEscapeParameter(rawText);
     }
 
-    private String visitStringLiteral(ParseTree ctx) {
-        String text = ctx.getText();
-        return text.substring(1, text.length() - 1).replace("\"\"", "\"");
+    /**
+     * The grammar defines escapes as '\)' and '\\'.
+     *
+     * @param rawText The raw text from the ParameterText token.
+     * @return A clean, un-escaped string.
+     */
+    private String unEscapeParameter(String rawText) {
+        // This handles both \) -> ) and \\ -> \
+        // Note: replaceAll uses regex, so we must double-escape the backslashes
+        return rawText.replace("\\)", ")")
+                .replace("\\\\", "\\");
     }
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java
index 3c3a2b78a7b..f64ed5919d5 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java
@@ -1,44 +1,49 @@
 package org.keycloak.models.workflow;
 
 import java.util.List;
+import java.util.function.BiPredicate;
 
+import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.FederatedIdentityModel.FederatedIdentityCreatedEvent;
 import org.keycloak.models.FederatedIdentityModel.FederatedIdentityRemovedEvent;
+import org.keycloak.models.GroupModel;
 import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.RoleModel.RoleGrantedEvent;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.provider.ProviderEvent;
 
+import static org.keycloak.models.utils.KeycloakModelUtils.GROUP_PATH_SEPARATOR;
+
 public enum ResourceOperationType {
 
-    USER_ADD(OperationType.CREATE, EventType.REGISTER),
-    USER_LOGIN(EventType.LOGIN),
-    USER_FEDERATED_IDENTITY_ADD(FederatedIdentityCreatedEvent.class),
-    USER_FEDERATED_IDENTITY_REMOVE(FederatedIdentityRemovedEvent.class),
-    USER_GROUP_MEMBERSHIP_ADD(GroupMemberJoinEvent.class),
-    USER_ROLE_ADD(RoleGrantedEvent.class),
-    AD_HOC(new Class[] {});
+    USER_ADDED(List.of(OperationType.CREATE, EventType.REGISTER)),
+    USER_LOGGED_IN(List.of(EventType.LOGIN), userLoginPredicate()),
+    USER_FEDERATED_IDENTITY_ADDED(List.of(FederatedIdentityCreatedEvent.class), fedIdentityPredicate()),
+    USER_FEDERATED_IDENTITY_REMOVED(List.of(FederatedIdentityRemovedEvent.class), fedIdentityPredicate()),
+    USER_GROUP_MEMBERSHIP_ADDED(List.of(GroupMemberJoinEvent.class), groupMembershipPredicate()),
+    USER_GROUP_MEMBERSHIP_REMOVED(List.of(GroupModel.GroupMemberLeaveEvent.class), groupMembershipPredicate()),
+    USER_ROLE_ADDED(List.of(RoleGrantedEvent.class), roleMembershipPredicate()),
+    USER_ROLE_REMOVED(List.of(RoleModel.RoleRevokedEvent.class), roleMembershipPredicate()),
+    AD_HOC(List.of(new Class[] {}));
 
     private final List types;
     private final List deactivationTypes;
+    private final BiPredicate conditionPredicate;
 
-    ResourceOperationType(Enum... types) {
-        this.types = List.of(types);
+    ResourceOperationType(List types) {
+        this.types = types;
         this.deactivationTypes = List.of();
+        this.conditionPredicate = defaultPredicate();
     }
 
-    @SafeVarargs
-    ResourceOperationType(Class... types) {
-        this.types = List.of(types);
+    ResourceOperationType(List types, BiPredicate conditionPredicate) {
+        this.types = types;
         this.deactivationTypes = List.of();
-    }
-
-    ResourceOperationType(Class[] types, Class[] deactivationTypes) {
-        this.types = List.of(types);
-        this.deactivationTypes = List.of(deactivationTypes);
+        this.conditionPredicate = defaultPredicate().and(conditionPredicate);
     }
 
     public static ResourceOperationType toOperationType(Enum from) {
@@ -91,4 +96,77 @@ public enum ResourceOperationType {
         }
         return false;
     }
+
+    public boolean test(WorkflowEvent event, String detail) {
+        return conditionPredicate.test(event, detail);
+    }
+
+    private BiPredicate defaultPredicate() {
+        return (event, detail) -> event.getOperation().equals(this);
+    }
+
+    private static BiPredicate userLoginPredicate() {
+            return (event, detail) -> {
+                if (detail != null) {
+                    Event loginEvent = (Event) event.getEvent();
+                    return detail.equals(loginEvent.getClientId());
+                } else {
+                    return true;
+                }
+            };
+    }
+
+    private static BiPredicate groupMembershipPredicate() {
+        return (event, groupName) -> {
+            if (groupName != null) {
+                if (!groupName.startsWith(GROUP_PATH_SEPARATOR))
+                    groupName = GROUP_PATH_SEPARATOR + groupName;
+                ProviderEvent groupEvent = (ProviderEvent) event.getEvent();
+                if (groupEvent instanceof GroupMemberJoinEvent joinEvent) {
+                    return groupName.equals(KeycloakModelUtils.buildGroupPath(joinEvent.getGroup()));
+                } else if (groupEvent instanceof GroupModel.GroupMemberLeaveEvent leaveEvent) {
+                    return groupName.equals(KeycloakModelUtils.buildGroupPath(leaveEvent.getGroup()));
+                } else {
+                    return false;
+                }
+            } else {
+                return true;
+            }
+        };
+    }
+
+    private static BiPredicate roleMembershipPredicate() {
+        return (event, roleName) -> {
+            if (roleName != null) {
+                ProviderEvent roleEvent = (ProviderEvent) event.getEvent();
+                if (roleEvent instanceof RoleGrantedEvent roleGrantedEvent) {
+                    return roleName.equals(roleGrantedEvent.getRole().getName());
+                } else if (roleEvent instanceof RoleModel.RoleRevokedEvent roleRevokedEvent) {
+                    return roleName.equals(roleRevokedEvent.getRole().getName());
+                } else {
+                    return false;
+                }
+            } else {
+                return true;
+            }
+        };
+    }
+
+    private static BiPredicate fedIdentityPredicate() {
+        return (event, idpAlias) -> {
+            if (idpAlias != null) {
+                ProviderEvent fedIdentityEvent = (ProviderEvent) event.getEvent();
+                if (fedIdentityEvent instanceof FederatedIdentityModel.FederatedIdentityCreatedEvent fedIdentityCreatedEvent) {
+                    return idpAlias.equals(fedIdentityCreatedEvent.getFederatedIdentity().getIdentityProvider());
+                } else if (fedIdentityEvent instanceof FederatedIdentityRemovedEvent fedIdentityRemovedEvent) {
+                    return idpAlias.equals(fedIdentityRemovedEvent.getFederatedIdentity().getIdentityProvider());
+                } else {
+                    return false;
+                }
+            } else {
+                return true;
+            }
+        };
+    }
+
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java
index 650fafcdd5c..375fabefadc 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java
@@ -10,7 +10,7 @@ public interface WorkflowConditionProvider extends Provider {
 
     boolean evaluate(WorkflowEvent event);
 
-    default Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery query, Root userRoot) {
+    default Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery query, Root resourceRoot) {
         return null;
     }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java
index a0f724f8e82..46f6ebf2efc 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java
@@ -1,8 +1,5 @@
 package org.keycloak.models.workflow;
 
-import java.util.List;
-import java.util.Map;
-
 import org.keycloak.Config;
 import org.keycloak.common.Profile;
 import org.keycloak.models.KeycloakSession;
@@ -11,9 +8,7 @@ import org.keycloak.provider.ProviderFactory;
 
 public interface WorkflowConditionProviderFactory

extends ProviderFactory

, EnvironmentDependentProviderFactory { - P create(KeycloakSession session, Map> config); - - P create(KeycloakSession session, List configParameters); + P create(KeycloakSession session, String configParameter); @Override default P create(KeycloakSession session) { diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java index 95454cab9a0..4a89073ac8b 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java @@ -30,22 +30,18 @@ import org.keycloak.models.ModelValidationException; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowConstants; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; import org.keycloak.utils.StringUtil; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Stream; import static java.util.Optional.ofNullable; -import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ENABLED; import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_NAME; @@ -206,17 +202,9 @@ public class WorkflowsManager { return factory; } - public WorkflowConditionProvider getConditionProvider(String providerId, MultivaluedHashMap modelConfig) { + public WorkflowConditionProvider getConditionProvider(String providerId, String providerConfig) { WorkflowConditionProviderFactory providerFactory = getConditionProviderFactory(providerId); - Map> config = new HashMap<>(); - - for (Entry> configEntry : modelConfig.entrySet()) { - if (configEntry.getKey().startsWith(providerId)) { - config.put(configEntry.getKey().substring(providerId.length() + 1), configEntry.getValue()); - } - } - - return providerFactory.create(session, config); + return providerFactory.create(session, providerConfig); } public WorkflowConditionProviderFactory getConditionProviderFactory(String providerId) { @@ -401,35 +389,8 @@ public class WorkflowsManager { /* ======================= Workflows representation <-> model conversions and validations ======================== */ public WorkflowRepresentation toRepresentation(Workflow workflow) { - List conditions = toConditionRepresentation(workflow); List steps = getSteps(workflow.getId()).map(this::toRepresentation).toList(); - - return new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig(), conditions, steps); - } - - private List toConditionRepresentation(Workflow workflow) { - MultivaluedHashMap workflowConfig = ofNullable(workflow.getConfig()).orElse(new MultivaluedHashMap<>()); - List ids = workflowConfig.getOrDefault(CONFIG_CONDITIONS, List.of()); - - if (ids.isEmpty()) { - return null; - } - - List conditions = new ArrayList<>(); - - for (String id : ids) { - MultivaluedHashMap config = new MultivaluedHashMap<>(); - - for (Entry> configEntry : workflowConfig.entrySet()) { - String key = configEntry.getKey(); - if (key.startsWith(id + ".")) { - config.put(key.substring(id.length() + 1), configEntry.getValue()); - } - } - conditions.add(new WorkflowConditionRepresentation(id, config)); - } - - return conditions; + return new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig(), steps); } private WorkflowStepRepresentation toRepresentation(WorkflowStep step) { @@ -440,25 +401,11 @@ public class WorkflowsManager { validateWorkflow(rep); MultivaluedHashMap config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); - List conditions = ofNullable(rep.getConditions()).orElse(List.of()); - - for (WorkflowConditionRepresentation condition : conditions) { - validateField(condition, "uses", condition.getUses()); - String conditionProviderId = condition.getUses(); - getConditionProviderFactory(conditionProviderId); - config.computeIfAbsent(CONFIG_CONDITIONS, key -> new ArrayList<>()).add(conditionProviderId); - - for (Entry> configEntry : condition.getConfig().entrySet()) { - config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue()); - } - } - if (rep.isCancelIfRunning()) { config.putSingle(WorkflowConstants.CONFIG_CANCEL_IF_RUNNING, "true"); } Workflow workflow = addWorkflow(new Workflow(rep.getUses(), config)); - addSteps(workflow, rep.getSteps()); return workflow; @@ -472,8 +419,8 @@ public class WorkflowsManager { private void validateWorkflow(WorkflowRepresentation rep) { validateField(rep, "name", rep.getName()); - - validateEvents(rep.getOnValues()); + //TODO: validate event and resource conditions (`on` and `if` properties) using the providers with a custom evaluator that calls validate on + // each condition provider used in the expression. // if a workflow has a restart step, at least one of the previous steps must be scheduled to prevent an infinite loop of immediate executions List steps = ofNullable(rep.getSteps()).orElse(List.of()); @@ -504,16 +451,6 @@ public class WorkflowsManager { } } - private static void validateEvents(List events) { - for (String event : ofNullable(events).orElse(List.of())) { - try { - ResourceOperationType.valueOf(event.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new WorkflowInvalidStateException("Invalid event type: " + event); - } - } - } - private void validateStep(WorkflowStep step) throws ModelValidationException { if (step.getAfter() < 0) { throw new ModelValidationException("Step 'after' time condition cannot be negative."); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java index d5c76b41a87..f483a6d2049 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java @@ -53,7 +53,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.workflows.WorkflowStateRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectClient; import org.keycloak.testframework.annotations.InjectRealm; @@ -121,6 +120,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest { private static final String IDP_OIDC_ALIAS = "kc-oidc-idp"; private static final String IDP_OIDC_PROVIDER_ID = "keycloak-oidc"; + private static final String IDP_CONDITION = IdentityProviderWorkflowConditionFactory.ID + "(" + IDP_OIDC_ALIAS + ")"; private static final String CLIENT_ID = "brokerapp"; private static final String CLIENT_SECRET = "secret"; @@ -130,11 +130,8 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest { consumerRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.toString()) - .onConditions(WorkflowConditionRepresentation.create() - .of(IdentityProviderWorkflowConditionFactory.ID) - .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) - .build()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) + .onCondition(IDP_CONDITION) .withSteps( WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) @@ -167,11 +164,8 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest { consumerRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.toString()) - .onConditions(WorkflowConditionRepresentation.create() - .of(IdentityProviderWorkflowConditionFactory.ID) - .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) - .build()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) + .onCondition(IDP_CONDITION) .withSteps( WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) @@ -239,11 +233,8 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest { public void testAddRemoveFedIdentityAffectsWorkflowAssociation() { consumerRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_FEDERATED_IDENTITY_ADD.toString()) - .onConditions(WorkflowConditionRepresentation.create() - .of(IdentityProviderWorkflowConditionFactory.ID) - .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) - .build()) + .onEvent(ResourceOperationType.USER_FEDERATED_IDENTITY_ADDED.toString()) + .onCondition(IDP_CONDITION) .withSteps( WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/ExpressionConditionWorkflowTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/ExpressionConditionWorkflowTest.java index b9469f8182f..4bbc3d2c7a1 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/ExpressionConditionWorkflowTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/ExpressionConditionWorkflowTest.java @@ -10,14 +10,11 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory; -import org.keycloak.models.workflow.ResourceOperationType; import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; import org.keycloak.models.workflow.WorkflowsManager; -import org.keycloak.models.workflow.conditions.ExpressionWorkflowConditionFactory; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.workflows.WorkflowSetRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; @@ -79,16 +76,8 @@ public class ExpressionConditionWorkflowTest { public void testExpressionCondition() { // create a couple of groups - String engineeringGroup; - String contractorsGroup; - try (Response response = managedRealm.admin().groups().add(GroupConfigBuilder.create() - .name("engineering").build())) { - engineeringGroup = ApiUtil.getCreatedId(response); - } - try (Response response = managedRealm.admin().groups().add(GroupConfigBuilder.create() - .name("contractors").build())) { - contractorsGroup = ApiUtil.getCreatedId(response); - } + managedRealm.admin().groups().add(GroupConfigBuilder.create().name("engineering").build()).close(); + managedRealm.admin().groups().add(GroupConfigBuilder.create().name("contractors").build()).close(); // create a few users with different attributes, roles and group memberships addUser("bwayne", "Bruce", "Wayne", List.of("developer", "admin"), List.of("engineering"), @@ -102,7 +91,7 @@ public class ExpressionConditionWorkflowTest { // we want to match members of engineering group OR users with admin role, but not those who are members of contractors group OR have attribute status=inactive // so only user bwayne should match this condition - String expression = "(is-member-of(\"" + engineeringGroup + "\") OR has-role(\"admin\")) AND !(is-member-of(\"" + contractorsGroup + "\") OR has-user-attribute(\"status\", \"inactive\"))"; + String expression = "(is-member-of(engineering) OR has-role(admin)) AND !(is-member-of(contractors) OR has-user-attribute(status:inactive))"; String workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", true); // matches all criteria @@ -112,7 +101,7 @@ public class ExpressionConditionWorkflowTest { managedRealm.admin().workflows().workflow(workflowId).delete().close(); // now we want to match users with attribute title=partner engineer OR users in the role tester - expression = "has-user-attribute(\"title\", \"partner engineer\") OR has-role(\"tester\")"; + expression = "has-user-attribute(title:partner engineer) OR has-role(tester)"; workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", false); // is not a partner engineer nor has role tester @@ -122,7 +111,7 @@ public class ExpressionConditionWorkflowTest { managedRealm.admin().workflows().workflow(workflowId).delete().close(); // now we want to match users who are tester and have attribute key=value1, value2 - expression = "has-role(\"tester\") AND has-user-attribute(\"key\", \"value1,value2\")"; + expression = "has-role(tester) AND has-user-attribute(key:value1,value2)"; workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", false); // is not a tester @@ -132,7 +121,7 @@ public class ExpressionConditionWorkflowTest { managedRealm.admin().workflows().workflow(workflowId).delete().close(); // now we want to match users who are not testers and also are not managers - expression = "!has-role(\"tester\") AND !has-user-attribute(\"title\", \"manager\")"; + expression = "!has-role(tester) AND !has-user-attribute(title:manager)"; workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", false); // is a manager @@ -142,7 +131,7 @@ public class ExpressionConditionWorkflowTest { managedRealm.admin().workflows().workflow(workflowId).delete().close(); // same thing but using the OR condition with negation - results should be equivalent - expression = "!(has-role(\"tester\") OR has-user-attribute(\"title\", \"manager\"))"; + expression = "!(has-role(tester) OR has-user-attribute(title:manager))"; workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", false); @@ -152,7 +141,7 @@ public class ExpressionConditionWorkflowTest { managedRealm.admin().workflows().workflow(workflowId).delete().close(); // a malformed expression should cause the condition to evaluate to false and the step should not run for all users - expression = ")(has-role(\"tester\") AND OR has-user-attribute(\"key\", \"value1,value2\")"; + expression = ")(has-role(tester) AND OR has-user-attribute(key, value1,value2)"; workflowId = createWorkflow(expression); checkWorkflowRunsForUser("bwayne", false); @@ -176,7 +165,7 @@ public class ExpressionConditionWorkflowTest { RealmModel realm = configureSessionContext(session); try { - // set offset to 6 days - set attribute step should run now but only for user-4 as they are the only one matching the condition + // set offset to 6 days to trigger the scheduled step (which is set to run after 5 days) Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); new WorkflowsManager(session).runScheduledSteps(); } finally { @@ -225,11 +214,8 @@ public class ExpressionConditionWorkflowTest { WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create() .of(EventBasedWorkflowProviderFactory.ID) .name(EventBasedWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.name()) - .onConditions(WorkflowConditionRepresentation.create() - .of(ExpressionWorkflowConditionFactory.ID) - .withConfig(Map.of(ExpressionWorkflowConditionFactory.EXPRESSION, List.of(expression))) - .build()) + .onEvent("user-logged-in(test-app)") + .onCondition(expression) .withSteps( WorkflowStepRepresentation.create() .of(SetUserAttributeStepProviderFactory.ID) diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java index 1c7e92c65b5..ecb80501100 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java @@ -31,7 +31,6 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.workflows.WorkflowSetRepresentation; import org.keycloak.representations.workflows.WorkflowStateRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; @@ -56,6 +55,9 @@ public class GroupMembershipJoinWorkflowTest { @InjectRealm(lifecycle = LifeCycle.METHOD) ManagedRealm managedRealm; + private static final String GROUP_NAME = "generic-group"; + private static final String GROUP_CONDITION = GroupMembershipWorkflowConditionFactory.ID + "(" + GROUP_NAME + ")"; + @Test public void testEventsOnGroupMembershipJoin() { UPConfig upConfig = managedRealm.admin().users().userProfile().getConfiguration(); @@ -71,11 +73,8 @@ public class GroupMembershipJoinWorkflowTest { WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create() .of(EventBasedWorkflowProviderFactory.ID) .name(EventBasedWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_ADD.name()) - .onConditions(WorkflowConditionRepresentation.create() - .of(GroupMembershipWorkflowConditionFactory.ID) - .withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId) - .build()) + .onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_ADDED.name()) + .onCondition(GROUP_CONDITION) .withSteps( WorkflowStepRepresentation.create() .of(SetUserAttributeStepProviderFactory.ID) @@ -102,7 +101,7 @@ public class GroupMembershipJoinWorkflowTest { userResource.joinGroup(groupId); runOnServer.run((session -> { - RealmModel realm = configureSessionContext(session); + configureSessionContext(session); WorkflowsManager manager = new WorkflowsManager(session); try { @@ -130,11 +129,8 @@ public class GroupMembershipJoinWorkflowTest { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.toString()) - .onConditions(WorkflowConditionRepresentation.create() - .of(GroupMembershipWorkflowConditionFactory.ID) - .withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId) - .build()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) + .onCondition(GROUP_CONDITION) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(1)) @@ -159,12 +155,11 @@ public class GroupMembershipJoinWorkflowTest { WorkflowStateRepresentation status = workflowRep.getState(); assertThat(status, notNullValue()); assertThat(status.getErrors(), hasSize(1)); - assertThat(status.getErrors().get(0), containsString("Group with id %s does not exist.".formatted(groupId))); + assertThat(status.getErrors().get(0), containsString("Group with name %s does not exist.".formatted("generic-group"))); } - private static RealmModel configureSessionContext(KeycloakSession session) { + private static void configureSessionContext(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName(REALM_NAME); session.getContext().setRealm(realm); - return realm; } } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java index 70dad90c376..1b83975f804 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java @@ -5,11 +5,9 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory.EXPECTED_ROLES; import java.time.Duration; import java.util.List; -import java.util.Map; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; @@ -31,7 +29,6 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.workflows.WorkflowSetRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; @@ -126,26 +123,23 @@ public class RoleWorkflowConditionTest { } private void createWorkflow(String... expectedValues) { - createWorkflow(Map.of(EXPECTED_ROLES, List.of(expectedValues))); + createWorkflow(List.of(expectedValues)); } private void createWorkflow(List expectedValues) { - createWorkflow(Map.of(EXPECTED_ROLES, expectedValues)); - } - private void createWorkflow(Map> attributes) { - for (String roleName : attributes.getOrDefault(EXPECTED_ROLES, List.of())) { + for (String roleName : expectedValues) { createRoleIfNotExists(roleName); } + String roleCondition = expectedValues.stream() + .map(role -> RoleWorkflowConditionFactory.ID + "(" + role + ")") + .reduce((a, b) -> a + " AND " + b).orElse(null); WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create() .of(EventBasedWorkflowProviderFactory.ID) .name(EventBasedWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ROLE_ADD.name()) - .onConditions(WorkflowConditionRepresentation.create() - .of(RoleWorkflowConditionFactory.ID) - .withConfig(attributes) - .build()) + .onEvent(ResourceOperationType.USER_ROLE_ADDED.name()) + .onCondition(roleCondition) .withSteps( WorkflowStepRepresentation.create() .of(SetUserAttributeStepProviderFactory.ID) diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java index 3436c60f54c..cf0735f6b8f 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java @@ -28,7 +28,6 @@ import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; import org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory; import org.keycloak.representations.workflows.WorkflowSetRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; @@ -133,14 +132,16 @@ public class UserAttributeWorkflowConditionTest { } private void createWorkflow(Map> attributes) { + String attributeCondition = attributes.keySet().stream() + .map(key -> UserAttributeWorkflowConditionFactory.ID + "(" + key + ":" + String.join(",", attributes.get(key)) + ")") + .reduce((a, b) -> a + " AND " + b) + .orElse(null); + WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create() .of(EventBasedWorkflowProviderFactory.ID) .name(EventBasedWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ADD.name()) - .onConditions(WorkflowConditionRepresentation.create() - .of(UserAttributeWorkflowConditionFactory.ID) - .withConfig(attributes) - .build()) + .onEvent(ResourceOperationType.USER_ADDED.name()) + .onCondition(attributeCondition) .withSteps( WorkflowStepRepresentation.create() .of(SetUserAttributeStepProviderFactory.ID) 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 92b0e0515ef..e02c475143c 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 { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.toString()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) .concurrency().cancelIfRunning() // this setting enables restarting the workflow .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) @@ -176,7 +176,7 @@ public class UserSessionRefreshTimeWorkflowTest { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID + "_1") - .onEvent(ResourceOperationType.USER_LOGIN.toString()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -186,7 +186,7 @@ public class UserSessionRefreshTimeWorkflowTest { ) .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .name(UserSessionRefreshTimeWorkflowProviderFactory.ID + "_2") - .onEvent(ResourceOperationType.USER_LOGIN.toString()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(10)) @@ -233,11 +233,10 @@ public class UserSessionRefreshTimeWorkflowTest { RealmModel realm = configureSessionContext(session); WorkflowsManager manager = new WorkflowsManager(session); - UserModel user = session.users().getUserByUsername(realm, username); try { Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds())); manager.runScheduledSteps(); - user = session.users().getUserByUsername(realm, username); + UserModel user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); } finally { Time.setOffset(0); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java index 0245f1a4365..dea907eb45a 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java @@ -30,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.UUID; @@ -72,7 +71,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.workflows.WorkflowConstants; import org.keycloak.representations.workflows.WorkflowSetRepresentation; import org.keycloak.representations.workflows.WorkflowStepRepresentation; -import org.keycloak.representations.workflows.WorkflowConditionRepresentation; import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectAdminClient; import org.keycloak.testframework.annotations.InjectKeycloakUrls; @@ -192,7 +190,7 @@ public class WorkflowManagementTest { workflows.create(WorkflowRepresentation.create() .of(UserCreationTimeWorkflowProviderFactory.ID) .name(UserCreationTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ADD.toString()) + .onEvent(ResourceOperationType.USER_ADDED.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -202,7 +200,7 @@ public class WorkflowManagementTest { ) .of(EventBasedWorkflowProviderFactory.ID) .name(EventBasedWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_LOGIN.toString()) + .onEvent(ResourceOperationType.USER_LOGGED_IN.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -269,18 +267,14 @@ public class WorkflowManagementTest { // now let's try to update another property that we can't update String previousOn = workflow.getOn(); - workflow.setOn(ResourceOperationType.USER_LOGIN.toString()); + workflow.setOn(ResourceOperationType.USER_LOGGED_IN.toString()); try (Response response = workflows.workflow(workflow.getId()).update(workflow)) { assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); } // restore previous value, but change the conditions workflow.setOn(previousOn); - workflow.setConditions(Collections.singletonList( - WorkflowConditionRepresentation.create().of(IdentityProviderWorkflowConditionFactory.ID) - .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, "someidp") - .build() - )); + workflow.setConditions(IdentityProviderWorkflowConditionFactory.ID + "(someidp)"); try (Response response = workflows.workflow(workflow.getId()).update(workflow)) { assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); } @@ -299,7 +293,7 @@ public class WorkflowManagementTest { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserCreationTimeWorkflowProviderFactory.ID) .name(UserCreationTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ADD.toString()) + .onEvent(ResourceOperationType.USER_ADDED.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -376,11 +370,8 @@ public class WorkflowManagementTest { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserCreationTimeWorkflowProviderFactory.ID) .name(UserCreationTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_FEDERATED_IDENTITY_ADD.name()) - .onConditions(WorkflowConditionRepresentation.create() - .of(IdentityProviderWorkflowConditionFactory.ID) - .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, "someidp") - .build()) + .onEvent(ResourceOperationType.USER_FEDERATED_IDENTITY_ADDED.name()) + .onCondition(IdentityProviderWorkflowConditionFactory.ID + "(someidp)") .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -476,7 +467,7 @@ public class WorkflowManagementTest { // create a test workflow managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserCreationTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ADD.toString()) + .onEvent(ResourceOperationType.USER_ADDED.toString()) .name("test-workflow") .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) @@ -593,7 +584,7 @@ public class WorkflowManagementTest { managedRealm.admin().workflows().create(WorkflowRepresentation.create() .of(UserCreationTimeWorkflowProviderFactory.ID) .name(UserCreationTimeWorkflowProviderFactory.ID) - .onEvent(ResourceOperationType.USER_ADD.toString()) + .onEvent(ResourceOperationType.USER_ADDED.toString()) .withSteps( WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) @@ -905,7 +896,7 @@ public class WorkflowManagementTest { .build() ).build(); - Client httpClient = Keycloak.getClientProvider().newRestEasyClient(null, null, true);; + Client httpClient = Keycloak.getClientProvider().newRestEasyClient(null, null, true); WebTarget target = httpClient.target(keycloakUrls.getBaseUrl().toString()) .path("admin") .path("realms")