mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Improve the Workflow JSON schema
Closes #42697 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
54d2451b35
commit
fe8fce859d
@ -18,6 +18,7 @@
|
||||
package org.keycloak.common.util.reflections;
|
||||
|
||||
import java.beans.Introspector;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
@ -1051,4 +1052,38 @@ public class Reflections {
|
||||
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
public static <T> T convertValueToType(Object value, Class<T> type) {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
|
||||
} else if (value instanceof String) {
|
||||
if (type == String.class) {
|
||||
return type.cast(value);
|
||||
} else if (type == Boolean.class) {
|
||||
return type.cast(Boolean.parseBoolean(value.toString()));
|
||||
} else if (type == Integer.class) {
|
||||
return type.cast(Integer.parseInt(value.toString()));
|
||||
} else if (type == Long.class) {
|
||||
return type.cast(Long.parseLong(value.toString()));
|
||||
}
|
||||
} else if (value instanceof Number) {
|
||||
if (type == Integer.class) {
|
||||
return type.cast(((Number) value).intValue());
|
||||
} else if (type == Long.class) {
|
||||
return type.cast(((Number) value).longValue());
|
||||
} else if (type == String.class) {
|
||||
return type.cast(value.toString());
|
||||
}
|
||||
} else if (value instanceof Boolean) {
|
||||
if (type == Boolean.class) {
|
||||
return type.cast(value);
|
||||
} else if (type == String.class) {
|
||||
return type.cast(value);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unable to handle type [" + type + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,138 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import static org.keycloak.common.util.reflections.Reflections.isArrayType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.reflections.Reflections;
|
||||
|
||||
public abstract class AbstractWorkflowComponentRepresentation {
|
||||
|
||||
private String id;
|
||||
private String uses;
|
||||
|
||||
@JsonProperty("with")
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
|
||||
public AbstractWorkflowComponentRepresentation(String id, String uses, MultivaluedHashMap<String, String> config) {
|
||||
this.id = id;
|
||||
this.uses = uses;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUses() {
|
||||
return this.uses;
|
||||
}
|
||||
|
||||
public void setUses(String uses) {
|
||||
this.uses = uses;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setConfig(MultivaluedHashMap<String, String> config) {
|
||||
if (this.config == null) {
|
||||
this.config = config;
|
||||
}
|
||||
this.config.putAll(config);
|
||||
}
|
||||
|
||||
public void setConfig(String key, String value) {
|
||||
setConfig(key, Collections.singletonList(value));
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public void setConfig(String key, List<String> values) {
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
}
|
||||
this.config.put(key, values);
|
||||
}
|
||||
|
||||
protected <T> T getConfigValue(String key, Class<T> type) {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Reflections.convertValueToType(config.getFirst(key), type);
|
||||
}
|
||||
|
||||
protected List<String> getConfigValues(String key) {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return config.get(key);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> T getConfigValuesOrSingle(String key) {
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> values = config.get(key);
|
||||
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (values.size() == 1) {
|
||||
return (T) values.get(0);
|
||||
}
|
||||
|
||||
return (T) values;
|
||||
}
|
||||
|
||||
protected void setConfigValue(String key, Object... values) {
|
||||
if (values == null || values.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
}
|
||||
|
||||
if (isArrayType(values.getClass())) {
|
||||
this.config.put(key, Arrays.stream(values).map(Object::toString).collect(Collectors.toList()));
|
||||
} else {
|
||||
this.config.putSingle(key, values[0].toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected void setConfigValue(String key, List<String> values) {
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
}
|
||||
this.config.put(key, values);
|
||||
}
|
||||
|
||||
protected void addConfigValue(String key, String value) {
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
}
|
||||
|
||||
this.config.add(key, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
public final class MultivaluedHashMapValueDeserializer extends JsonDeserializer {
|
||||
|
||||
@Override
|
||||
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<>();
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
|
||||
if (node.isObject()) {
|
||||
for (Entry<String, JsonNode> property : node.properties()) {
|
||||
String key = property.getKey();
|
||||
JsonNode values = property.getValue();
|
||||
|
||||
if (values.isArray()) {
|
||||
for (JsonNode value : values) {
|
||||
map.add(key, value.asText());
|
||||
}
|
||||
} else {
|
||||
map.add(key, values.asText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
public final class MultivaluedHashMapValueSerializer extends JsonSerializer<MultivaluedHashMap<String, String>> {
|
||||
|
||||
@Override
|
||||
public void serialize(MultivaluedHashMap<String, String> map, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
Set<String> ignoredProperties = getIgnoredProperties(gen);
|
||||
|
||||
gen.writeStartObject();
|
||||
|
||||
for (Entry<String, List<String>> entry : map.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
||||
if (ignoredProperties.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> values = entry.getValue();
|
||||
|
||||
if (values.size() == 1) {
|
||||
String value = values.get(0);
|
||||
|
||||
if (Boolean.TRUE.toString().equalsIgnoreCase(value) || Boolean.FALSE.toString().equalsIgnoreCase(value)) {
|
||||
gen.writeBooleanField(key, Boolean.parseBoolean(value));
|
||||
} else {
|
||||
gen.writeObjectField(key, value);
|
||||
}
|
||||
} else {
|
||||
gen.writeArrayFieldStart(key);
|
||||
for (String v : values) {
|
||||
gen.writeString(v);
|
||||
}
|
||||
gen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
gen.writeEndObject();
|
||||
}
|
||||
|
||||
private static Set<String> getIgnoredProperties(JsonGenerator gen) {
|
||||
Class<?> parentClazz = gen.currentValue().getClass();
|
||||
return Arrays.stream(parentClazz.getDeclaredMethods())
|
||||
.map(Method::getName)
|
||||
.filter(name -> name.startsWith("get"))
|
||||
.map(name -> name.substring(3).toLowerCase()).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@ -1,66 +1,41 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
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;
|
||||
|
||||
public class WorkflowConditionRepresentation {
|
||||
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();
|
||||
}
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private Map<String, List<String>> config;
|
||||
|
||||
public WorkflowConditionRepresentation() {
|
||||
// reflection
|
||||
super(null, null, null);
|
||||
}
|
||||
|
||||
public WorkflowConditionRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
public WorkflowConditionRepresentation(String condition) {
|
||||
this(condition, null);
|
||||
}
|
||||
|
||||
public WorkflowConditionRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
this(null, providerId, config);
|
||||
public WorkflowConditionRepresentation(String condition, MultivaluedHashMap<String, String> config) {
|
||||
super(null, condition, config);
|
||||
}
|
||||
|
||||
public WorkflowConditionRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public void setProviderId(String providerId) {
|
||||
this.providerId = providerId;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setConfig(Map<String, List<String>> config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public void setConfig(String key, String value) {
|
||||
if (this.config == null) {
|
||||
this.config = new HashMap<>();
|
||||
}
|
||||
this.config.put(key, Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public void setConfig(String key, List<String> values) {
|
||||
if (this.config == null) {
|
||||
this.config = new HashMap<>();
|
||||
}
|
||||
this.config.put(key, values);
|
||||
@Override
|
||||
@JsonSerialize(using = MultivaluedHashMapValueSerializer.class)
|
||||
@JsonDeserialize(using = MultivaluedHashMapValueDeserializer.class)
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return super.getConfig();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@ -77,13 +52,13 @@ public class WorkflowConditionRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, List<String> value) {
|
||||
action.setConfig(key, value);
|
||||
public Builder withConfig(String key, String... values) {
|
||||
action.setConfig(key, Arrays.asList(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(Map<String, List<String>> config) {
|
||||
action.setConfig(config);
|
||||
action.setConfig(new MultivaluedHashMap<>(config));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
public final class WorkflowConstants {
|
||||
|
||||
public static final String DEFAULT_WORKFLOW = "event-based-workflow";
|
||||
|
||||
public static final String CONFIG_USES = "uses";
|
||||
public static final String CONFIG_WITH = "with";
|
||||
|
||||
// Entry configuration keys for Workflow
|
||||
public static final String CONFIG_ON_EVENT = "on";
|
||||
public static final String CONFIG_RESET_ON = "reset-on";
|
||||
public static final String CONFIG_NAME = "name";
|
||||
public static final String CONFIG_RECURRING = "recurring";
|
||||
public static final String CONFIG_SCHEDULED = "scheduled";
|
||||
public static final String CONFIG_ENABLED = "enabled";
|
||||
public static final String CONFIG_CONDITIONS = "conditions";
|
||||
public static final String CONFIG_STEPS = "steps";
|
||||
public static final String CONFIG_ERROR = "error";
|
||||
public static final String CONFIG_STATE = "state";
|
||||
|
||||
// Entry configuration keys for WorkflowCondition
|
||||
public static final String CONFIG_IF = "if";
|
||||
|
||||
// Entry configuration keys for WorkflowStep
|
||||
public static final String CONFIG_AFTER = "after";
|
||||
public static final String CONFIG_PRIORITY = "priority";
|
||||
}
|
||||
@ -1,5 +1,18 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
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;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ON_EVENT;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RESET_ON;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_STATE;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_STEPS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WITH;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_USES;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -7,65 +20,107 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
public class WorkflowRepresentation {
|
||||
@JsonPropertyOrder({"id", CONFIG_NAME, CONFIG_USES, CONFIG_ENABLED, CONFIG_ON_EVENT, CONFIG_RESET_ON, CONFIG_SCHEDULED, CONFIG_RECURRING, CONFIG_IF, CONFIG_STEPS, CONFIG_STATE})
|
||||
@JsonIgnoreProperties(CONFIG_WITH)
|
||||
public final class WorkflowRepresentation extends AbstractWorkflowComponentRepresentation {
|
||||
|
||||
public static Builder create() {
|
||||
return new Builder();
|
||||
return new Builder().of(WorkflowConstants.DEFAULT_WORKFLOW);
|
||||
}
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<WorkflowStepRepresentation> steps;
|
||||
|
||||
@JsonProperty(CONFIG_IF)
|
||||
private List<WorkflowConditionRepresentation> conditions;
|
||||
|
||||
private WorkflowStateRepresentation state;
|
||||
|
||||
public WorkflowRepresentation() {
|
||||
// reflection
|
||||
super(null, null, null);
|
||||
}
|
||||
|
||||
public WorkflowRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
public WorkflowRepresentation(String id, String workflow, MultivaluedHashMap<String, String> config, List<WorkflowConditionRepresentation> conditions, List<WorkflowStepRepresentation> steps) {
|
||||
super(id, workflow, config);
|
||||
this.conditions = conditions;
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public WorkflowRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
this(null, providerId, config);
|
||||
public <T> T getOn() {
|
||||
return getConfigValuesOrSingle(CONFIG_ON_EVENT);
|
||||
}
|
||||
|
||||
public WorkflowRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = new MultivaluedHashMap<>(config);
|
||||
public void setOn(String... events) {
|
||||
setConfigValue(CONFIG_ON_EVENT, Arrays.asList(events));
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public void setOn(List<String> events) {
|
||||
setConfigValue(CONFIG_ON_EVENT, events);
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
@JsonIgnore
|
||||
public List<String> getOnValues() {
|
||||
return ofNullable(getConfigValues(CONFIG_ON_EVENT)).orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return this.providerId;
|
||||
@JsonProperty(CONFIG_RESET_ON)
|
||||
public <T> T getOnEventReset() {
|
||||
return getConfigValuesOrSingle(CONFIG_RESET_ON);
|
||||
}
|
||||
|
||||
public void setProviderId(String providerId) {
|
||||
this.providerId = providerId;
|
||||
@JsonIgnore
|
||||
public List<String> getOnEventsReset() {
|
||||
return ofNullable(getConfigValues(CONFIG_RESET_ON)).orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public void setOnEventReset(List<String> names) {
|
||||
setConfigValue(CONFIG_RESET_ON, names);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setOnEventReset(String... names) {
|
||||
setOnEventReset(Arrays.asList(names));
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return Optional.ofNullable(config).orElse(new MultivaluedHashMap<>()).getFirst("name");
|
||||
return getConfigValue(CONFIG_NAME, String.class);
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
setConfigValue(CONFIG_NAME, name);
|
||||
}
|
||||
this.config.putSingle("name", name);
|
||||
|
||||
public Boolean getRecurring() {
|
||||
return getConfigValue(CONFIG_RECURRING, Boolean.class);
|
||||
}
|
||||
|
||||
public void setRecurring(Boolean recurring) {
|
||||
setConfigValue(CONFIG_RECURRING, recurring);
|
||||
}
|
||||
|
||||
public Boolean getScheduled() {
|
||||
return getConfigValue(CONFIG_SCHEDULED, Boolean.class);
|
||||
}
|
||||
|
||||
public void setScheduled(Boolean scheduled) {
|
||||
setConfigValue(CONFIG_SCHEDULED, scheduled);
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return getConfigValue(CONFIG_ENABLED, Boolean.class);
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
setConfigValue(CONFIG_ENABLED, enabled);
|
||||
}
|
||||
|
||||
public void setConditions(List<WorkflowConditionRepresentation> conditions) {
|
||||
@ -84,96 +139,101 @@ public class WorkflowRepresentation {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
public WorkflowStateRepresentation getState() {
|
||||
if (state == null) {
|
||||
state = new WorkflowStateRepresentation(this);
|
||||
}
|
||||
|
||||
public void addStep(WorkflowStepRepresentation step) {
|
||||
if (steps == null) {
|
||||
steps = new ArrayList<>();
|
||||
if (state.getErrors().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
steps.add(step);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(WorkflowStateRepresentation state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String providerId;
|
||||
private final Map<String, List<String>> config = new HashMap<>();
|
||||
private List<WorkflowConditionRepresentation> conditions = new ArrayList<>();
|
||||
private final Map<String, List<WorkflowStepRepresentation>> steps = new HashMap<>();
|
||||
|
||||
private final Map<WorkflowRepresentation, List<WorkflowStepRepresentation>> steps = new HashMap<>();
|
||||
private List<Builder> builders = new ArrayList<>();
|
||||
private WorkflowRepresentation representation;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
private Builder(String providerId, List<Builder> builders) {
|
||||
this.providerId = providerId;
|
||||
private Builder(WorkflowRepresentation representation, List<Builder> builders) {
|
||||
this.representation = representation;
|
||||
this.builders = builders;
|
||||
}
|
||||
|
||||
public Builder of(String providerId) {
|
||||
Builder builder = new Builder(providerId, builders);
|
||||
WorkflowRepresentation representation = new WorkflowRepresentation();
|
||||
representation.setUses(providerId);
|
||||
Builder builder = new Builder(representation, builders);
|
||||
builders.add(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public Builder onEvent(String operation) {
|
||||
List<String> events = config.computeIfAbsent("events", k -> new ArrayList<>());
|
||||
|
||||
events.add(operation);
|
||||
|
||||
representation.addConfigValue(CONFIG_ON_EVENT, operation);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onConditions(WorkflowConditionRepresentation... condition) {
|
||||
if (conditions == null) {
|
||||
conditions = new ArrayList<>();
|
||||
}
|
||||
conditions.addAll(Arrays.asList(condition));
|
||||
representation.setConditions(Arrays.asList(condition));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
this.steps.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(steps));
|
||||
this.steps.computeIfAbsent(representation, (k) -> new ArrayList<>()).addAll(Arrays.asList(steps));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, String value) {
|
||||
config.put(key, Collections.singletonList(value));
|
||||
representation.addConfigValue(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, List<String> value) {
|
||||
config.put(key, value);
|
||||
public Builder withConfig(String key, List<String> values) {
|
||||
representation.setConfigValue(key, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder name(String name) {
|
||||
return withConfig("name", name);
|
||||
representation.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder immediate() {
|
||||
return withConfig("scheduled", "false");
|
||||
representation.setScheduled(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder recurring() {
|
||||
return withConfig("recurring", "true");
|
||||
representation.setRecurring(true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<WorkflowRepresentation> build() {
|
||||
public WorkflowSetRepresentation build() {
|
||||
List<WorkflowRepresentation> workflows = new ArrayList<>();
|
||||
|
||||
for (Builder builder : builders) {
|
||||
for (Entry<String, List<WorkflowStepRepresentation>> entry : builder.steps.entrySet()) {
|
||||
WorkflowRepresentation workflow = new WorkflowRepresentation(entry.getKey(), builder.config);
|
||||
if (builder.steps.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (Entry<WorkflowRepresentation, List<WorkflowStepRepresentation>> entry : builder.steps.entrySet()) {
|
||||
WorkflowRepresentation workflow = entry.getKey();
|
||||
|
||||
workflow.setSteps(entry.getValue());
|
||||
workflow.setConditions(builder.conditions);
|
||||
|
||||
workflows.add(workflow);
|
||||
}
|
||||
}
|
||||
|
||||
return workflows;
|
||||
return new WorkflowSetRepresentation(workflows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
|
||||
public final class WorkflowSetRepresentation {
|
||||
|
||||
@JsonUnwrapped
|
||||
private List<WorkflowRepresentation> workflows;
|
||||
|
||||
public WorkflowSetRepresentation() {
|
||||
|
||||
}
|
||||
|
||||
public WorkflowSetRepresentation(List<WorkflowRepresentation> workflows) {
|
||||
this.workflows = workflows;
|
||||
}
|
||||
|
||||
public void setWorkflows(List<WorkflowRepresentation> workflows) {
|
||||
this.workflows = workflows;
|
||||
}
|
||||
|
||||
public List<WorkflowRepresentation> getWorkflows() {
|
||||
return workflows;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ERROR;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class WorkflowStateRepresentation {
|
||||
|
||||
private List<String> errors = Collections.emptyList();
|
||||
|
||||
public WorkflowStateRepresentation() {}
|
||||
|
||||
public WorkflowStateRepresentation(WorkflowRepresentation workflow) {
|
||||
this.errors = ofNullable(workflow.getConfigValues(CONFIG_ERROR)).orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@ -1,77 +1,69 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_PRIORITY;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_STEPS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_USES;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WITH;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
|
||||
public class WorkflowStepRepresentation {
|
||||
|
||||
private static final String AFTER_KEY = "after";
|
||||
@JsonPropertyOrder({"id", CONFIG_USES, CONFIG_AFTER, CONFIG_PRIORITY, CONFIG_WITH, CONFIG_STEPS})
|
||||
public final class WorkflowStepRepresentation extends AbstractWorkflowComponentRepresentation {
|
||||
|
||||
public static Builder create() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<WorkflowStepRepresentation> steps;
|
||||
|
||||
public WorkflowStepRepresentation() {
|
||||
// reflection
|
||||
super(null, null, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
public WorkflowStepRepresentation(String step) {
|
||||
this(step, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String providerId, MultivaluedHashMap<String, String> config) {
|
||||
this(null, providerId, config, null);
|
||||
public WorkflowStepRepresentation(String step, MultivaluedHashMap<String, String> config) {
|
||||
this(null, step, config, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String id, String providerId, MultivaluedHashMap<String, String> config, List<WorkflowStepRepresentation> steps) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
public WorkflowStepRepresentation(String id, String step, MultivaluedHashMap<String, String> config, List<WorkflowStepRepresentation> steps) {
|
||||
super(id, step, config);
|
||||
|
||||
if (steps != null && !steps.isEmpty()) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public void setProviderId(String providerId) {
|
||||
this.providerId = providerId;
|
||||
}
|
||||
|
||||
@JsonSerialize(using = MultivaluedHashMapValueSerializer.class)
|
||||
@JsonDeserialize(using = MultivaluedHashMapValueDeserializer.class)
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
return super.getConfig();
|
||||
}
|
||||
|
||||
public void setConfig(MultivaluedHashMap<String, String> config) {
|
||||
this.config = config;
|
||||
public String getAfter() {
|
||||
return getConfigValue(CONFIG_AFTER, String.class);
|
||||
}
|
||||
|
||||
public void setConfig(String key, String value) {
|
||||
setConfig(key, Collections.singletonList(value));
|
||||
public void setAfter(long ms) {
|
||||
setConfig(CONFIG_AFTER, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public void setConfig(String key, List<String> values) {
|
||||
if (this.config == null) {
|
||||
this.config = new MultivaluedHashMap<>();
|
||||
}
|
||||
this.config.put(key, values);
|
||||
public String getPriority() {
|
||||
return getConfigValue(CONFIG_PRIORITY, String.class);
|
||||
}
|
||||
|
||||
private void setAfter(long ms) {
|
||||
setConfig(AFTER_KEY, String.valueOf(ms));
|
||||
public void setPriority(long ms) {
|
||||
setConfig(CONFIG_PRIORITY, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
@ -96,9 +88,14 @@ public class WorkflowStepRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder id(String id) {
|
||||
step.setId(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder before(WorkflowStepRepresentation targetStep, Duration timeBeforeTarget) {
|
||||
// Calculate absolute time: targetStep.after - timeBeforeTarget
|
||||
String targetAfter = targetStep.getConfig().get(AFTER_KEY).get(0);
|
||||
String targetAfter = targetStep.getConfig().get(CONFIG_AFTER).get(0);
|
||||
long targetTime = Long.parseLong(targetAfter);
|
||||
long thisTime = targetTime - timeBeforeTarget.toMillis();
|
||||
step.setAfter(thisTime);
|
||||
@ -110,13 +107,13 @@ public class WorkflowStepRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
step.setSteps(Arrays.asList(steps));
|
||||
public Builder withConfig(String key, String... value) {
|
||||
step.setConfig(key, Arrays.asList(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, List<String> values) {
|
||||
step.setConfig(key, values);
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
step.setSteps(Arrays.asList(steps));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,172 @@
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
public class WorkflowDefinitionTest {
|
||||
|
||||
@Test
|
||||
public void testFullDefinition() throws IOException {
|
||||
WorkflowRepresentation expected = new WorkflowRepresentation();
|
||||
|
||||
expected.setId("workflow-id");
|
||||
expected.setUses("my-provider");
|
||||
expected.setName("my-name");
|
||||
expected.setOn("event");
|
||||
expected.setOnEventReset("event-reset-1", "event-reset-2");
|
||||
expected.setSteps(null);
|
||||
expected.setConditions(null);
|
||||
expected.setRecurring(true);
|
||||
expected.setScheduled(true);
|
||||
expected.setEnabled(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")
|
||||
.id("1")
|
||||
.withConfig("key1", "v1")
|
||||
.after(Duration.ofSeconds(10))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of("step-2")
|
||||
.id("2")
|
||||
.withConfig("key1", "v1", "v2")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of("step-1")
|
||||
.id("3")
|
||||
.withConfig("key1", "v1")
|
||||
.build()));
|
||||
|
||||
String json = JsonSerialization.writeValueAsPrettyString(expected);
|
||||
|
||||
WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class);
|
||||
|
||||
assertEquals(expected.getId(), actual.getId());
|
||||
assertEquals(expected.getUses(), actual.getUses());
|
||||
assertTrue(actual.getOn() instanceof String);
|
||||
assertEquals(expected.getOn(), (String) actual.getOn());
|
||||
assertArrayEquals(((List) expected.getOnEventReset()).toArray(), ((List) actual.getOnEventReset()).toArray());
|
||||
assertEquals(expected.getName(), actual.getName());
|
||||
assertEquals(expected.getRecurring(), actual.getRecurring());
|
||||
assertEquals(expected.getScheduled(), actual.getScheduled());
|
||||
assertEquals(expected.getEnabled(), actual.getEnabled());
|
||||
|
||||
List<WorkflowConditionRepresentation> actualConditions = actual.getConditions();
|
||||
assertNotNull(actualConditions);
|
||||
actualConditions = actualConditions.stream().sorted(Comparator.comparing(WorkflowConditionRepresentation::getUses)).collect(Collectors.toList());
|
||||
List<WorkflowConditionRepresentation> 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"));
|
||||
|
||||
|
||||
List<WorkflowStepRepresentation> actualSteps = actual.getSteps();
|
||||
assertNotNull(actualSteps);
|
||||
actualSteps = actualSteps.stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
List<WorkflowStepRepresentation> expectedSteps = expected.getSteps().stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
|
||||
assertEquals(expectedSteps.size(), actualSteps.size());
|
||||
assertEquals(expectedSteps.get(0).getUses(), actualSteps.get(0).getUses());
|
||||
assertEquals(expectedSteps.get(0).getConfig().get("key1"), actualSteps.get(0).getConfig().get("key1"));
|
||||
assertEquals(expectedSteps.get(1).getConfig().get("key1"), actualSteps.get(1).getConfig().get("key1"));
|
||||
assertEquals(expectedSteps.get(2).getConfig().get("key1"), actualSteps.get(2).getConfig().get("key1"));
|
||||
|
||||
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");
|
||||
|
||||
String json = JsonSerialization.writeValueAsPrettyString(expected);
|
||||
WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class);
|
||||
assertTrue(actual.getOn() instanceof String);
|
||||
assertEquals("event", actual.getOn());
|
||||
|
||||
System.out.println(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregatedAction() throws IOException {
|
||||
WorkflowRepresentation expected = new WorkflowRepresentation();
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
config.put("k1", Collections.singletonList("v1"));
|
||||
WorkflowStepRepresentation aggregated = new WorkflowStepRepresentation("step-1", config);
|
||||
|
||||
config = new MultivaluedHashMap<>();
|
||||
config.put("k1", Collections.singletonList("v1"));
|
||||
aggregated.setSteps(Arrays.asList(new WorkflowStepRepresentation("sub-step-1", new MultivaluedHashMap<>(config)), new WorkflowStepRepresentation("sub-step-2", new MultivaluedHashMap<>(config))));
|
||||
|
||||
expected.setSteps(Collections.singletonList(aggregated));
|
||||
|
||||
String json = JsonSerialization.writeValueAsPrettyString(expected);
|
||||
WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class);
|
||||
|
||||
List<WorkflowStepRepresentation> actualSteps = actual.getSteps();
|
||||
assertNotNull(actualSteps);
|
||||
actualSteps = actualSteps.stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
List<WorkflowStepRepresentation> expectedSteps = expected.getSteps().stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
|
||||
assertEquals(expectedSteps.size(), actualSteps.size());
|
||||
assertEquals(expectedSteps.get(0).getUses(), actualSteps.get(0).getUses());
|
||||
assertEquals(expectedSteps.get(0).getConfig().get("k1"), actualSteps.get(0).getConfig().get("k1"));
|
||||
assertEquals(expectedSteps.get(0).getSteps().size(), actualSteps.get(0).getSteps().size());
|
||||
assertEquals(expectedSteps.get(0).getSteps().get(0).getConfig().get("k1"), actualSteps.get(0).getSteps().get(0).getConfig().get("k1"));
|
||||
|
||||
System.out.println(json);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
package org.keycloak.admin.client.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
@ -9,8 +11,7 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.representations.workflows.WorkflowSetRepresentation;
|
||||
|
||||
public interface WorkflowsResource {
|
||||
|
||||
@ -18,9 +19,10 @@ public interface WorkflowsResource {
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response create(WorkflowRepresentation representation);
|
||||
|
||||
@Path("set")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response create(List<WorkflowRepresentation> representation);
|
||||
Response create(WorkflowSetRepresentation representation);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -26,6 +28,7 @@ import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@ -70,7 +73,8 @@ public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowPro
|
||||
}
|
||||
|
||||
private List<Predicate> getConditionsPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> path) {
|
||||
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
|
||||
MultivaluedHashMap<String, String> config = getModel().getConfig();
|
||||
List<String> conditions = config.getOrDefault(CONFIG_CONDITIONS, List.of());
|
||||
|
||||
if (conditions.isEmpty()) {
|
||||
return List.of();
|
||||
@ -79,7 +83,7 @@ public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowPro
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
for (String providerId : conditions) {
|
||||
WorkflowConditionProvider condition = resolveCondition(providerId);
|
||||
WorkflowConditionProvider condition = getManager().getConditionProvider(providerId, config);
|
||||
Predicate predicate = condition.toPredicate(cb, query, path);
|
||||
|
||||
if (predicate != null) {
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.HashMap;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ON_EVENT;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RESET_ON;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
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
|
||||
@ -42,25 +44,13 @@ public class EventBasedWorkflowProvider implements WorkflowProvider {
|
||||
return evaluate(event);
|
||||
}
|
||||
|
||||
protected boolean isActivationEvent(WorkflowEvent event) {
|
||||
ResourceOperationType operation = event.getOperation();
|
||||
|
||||
if (ResourceOperationType.AD_HOC.equals(operation)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> events = model.getConfig().getOrDefault("events", List.of());
|
||||
|
||||
return events.contains(operation.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deactivateOnEvent(WorkflowEvent event) {
|
||||
if (!supports(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> events = model.getConfig().getOrDefault("events", List.of());
|
||||
List<String> events = model.getConfig().getOrDefault(CONFIG_ON_EVENT, List.of());
|
||||
|
||||
for (String activationEvent : events) {
|
||||
ResourceOperationType a = ResourceOperationType.valueOf(activationEvent);
|
||||
@ -78,21 +68,16 @@ public class EventBasedWorkflowProvider implements WorkflowProvider {
|
||||
return isResetEvent(event) && evaluate(event);
|
||||
}
|
||||
|
||||
protected boolean isResetEvent(WorkflowEvent event) {
|
||||
boolean resetEventEnabled = Boolean.parseBoolean(getModel().getConfig().getFirstOrDefault("reset-event-enabled", Boolean.FALSE.toString()));
|
||||
return resetEventEnabled && isActivationEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
protected boolean evaluate(WorkflowEvent event) {
|
||||
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
|
||||
List<String> conditions = getModel().getConfig().getOrDefault(CONFIG_CONDITIONS, List.of());
|
||||
|
||||
for (String providerId : conditions) {
|
||||
WorkflowConditionProvider condition = resolveCondition(providerId);
|
||||
WorkflowConditionProvider condition = manager.getConditionProvider(providerId, model.getConfig());
|
||||
|
||||
if (!condition.evaluate(event)) {
|
||||
return false;
|
||||
@ -102,29 +87,16 @@ public class EventBasedWorkflowProvider implements WorkflowProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected WorkflowConditionProvider resolveCondition(String providerId) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
WorkflowConditionProviderFactory<WorkflowConditionProvider> providerFactory = (WorkflowConditionProviderFactory<WorkflowConditionProvider>) sessionFactory.getProviderFactory(WorkflowConditionProvider.class, providerId);
|
||||
protected boolean isActivationEvent(WorkflowEvent event) {
|
||||
ResourceOperationType operation = event.getOperation();
|
||||
|
||||
if (providerFactory == null) {
|
||||
throw new IllegalStateException("Could not find condition provider: " + providerId);
|
||||
if (ResourceOperationType.AD_HOC.equals(operation)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<String, List<String>> config = new HashMap<>();
|
||||
List<String> events = model.getConfig().getOrDefault(CONFIG_ON_EVENT, List.of());
|
||||
|
||||
for (Entry<String, List<String>> configEntry : model.getConfig().entrySet()) {
|
||||
if (configEntry.getKey().startsWith(providerId)) {
|
||||
config.put(configEntry.getKey().substring(providerId.length() + 1), configEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
WorkflowConditionProvider condition = providerFactory.create(session, config);
|
||||
|
||||
if (condition == null) {
|
||||
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
|
||||
}
|
||||
|
||||
return condition;
|
||||
return events.contains(operation.name());
|
||||
}
|
||||
|
||||
protected ComponentModel getModel() {
|
||||
@ -134,4 +106,14 @@ public class EventBasedWorkflowProvider implements WorkflowProvider {
|
||||
protected KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
protected WorkflowsManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
protected boolean isResetEvent(WorkflowEvent event) {
|
||||
return model.getConfig()
|
||||
.getOrDefault(CONFIG_RESET_ON, List.of())
|
||||
.contains(event.getOperation().name());
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,12 +8,12 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class EventBasedWorkflowProviderFactory implements WorkflowProviderFactory {
|
||||
public class EventBasedWorkflowProviderFactory implements WorkflowProviderFactory<EventBasedWorkflowProvider> {
|
||||
|
||||
public static final String ID = "event-based-workflow";
|
||||
|
||||
@Override
|
||||
public WorkflowProvider create(KeycloakSession session, ComponentModel model) {
|
||||
public EventBasedWorkflowProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new EventBasedWorkflowProvider(session, model);
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class GroupMembershipWorkflowConditionFactory implements WorkflowConditionProviderFactory<GroupMembershipWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "group-membership-condition";
|
||||
public static final String ID = "is-member-of";
|
||||
public static final String EXPECTED_GROUPS = "groups";
|
||||
|
||||
@Override
|
||||
|
||||
@ -9,7 +9,7 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class IdentityProviderWorkflowConditionFactory implements WorkflowConditionProviderFactory<IdentityProviderWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "identity-provider-condition";
|
||||
public static final String ID = "has-identity-provider-link";
|
||||
public static final String EXPECTED_ALIASES = "alias";
|
||||
|
||||
@Override
|
||||
|
||||
@ -9,7 +9,7 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class RoleWorkflowConditionFactory implements WorkflowConditionProviderFactory<RoleWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "role-condition";
|
||||
public static final String ID = "has-role";
|
||||
public static final String EXPECTED_ROLES = "roles";
|
||||
|
||||
@Override
|
||||
|
||||
@ -9,7 +9,7 @@ import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class UserAttributeWorkflowConditionFactory implements WorkflowConditionProviderFactory<UserAttributeWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "user-attribute-condition";
|
||||
public static final String ID = "has-user-attribute";
|
||||
|
||||
@Override
|
||||
public UserAttributeWorkflowConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
|
||||
@ -10,17 +10,16 @@ import org.keycloak.models.FederatedIdentityModel.FederatedIdentityRemovedEvent;
|
||||
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.RoleModel.RoleGrantedEvent;
|
||||
import org.keycloak.models.RoleModel.RoleRevokedEvent;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
public enum ResourceOperationType {
|
||||
|
||||
USER_ADD(OperationType.CREATE, EventType.REGISTER),
|
||||
USER_LOGIN(EventType.LOGIN),
|
||||
USER_FEDERATED_IDENTITY_ADD(new Class[] {FederatedIdentityCreatedEvent.class}, new Class[] {FederatedIdentityRemovedEvent.class}),
|
||||
USER_FEDERATED_IDENTITY_ADD(FederatedIdentityCreatedEvent.class),
|
||||
USER_FEDERATED_IDENTITY_REMOVE(FederatedIdentityRemovedEvent.class),
|
||||
USER_GROUP_MEMBERSHIP_ADD(GroupMemberJoinEvent.class),
|
||||
USER_ROLE_ADD(new Class[] {RoleGrantedEvent.class}, new Class[] {RoleRevokedEvent.class}),
|
||||
USER_ROLE_ADD(RoleGrantedEvent.class),
|
||||
AD_HOC(new Class[] {});
|
||||
|
||||
private final List<Object> types;
|
||||
|
||||
@ -17,6 +17,11 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ENABLED;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ERROR;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -25,9 +30,6 @@ import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class Workflow {
|
||||
|
||||
public static final String SCHEDULED_KEY = "scheduled";
|
||||
public static final String RECURRING_KEY = "recurring";
|
||||
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private String providerId;
|
||||
private String id;
|
||||
@ -69,15 +71,15 @@ public class Workflow {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return config != null && Boolean.parseBoolean(config.getFirstOrDefault("enabled", "true"));
|
||||
return config != null && Boolean.parseBoolean(config.getFirstOrDefault(CONFIG_ENABLED, "true"));
|
||||
}
|
||||
|
||||
public boolean isRecurring() {
|
||||
return config != null && Boolean.parseBoolean(config.getFirst(RECURRING_KEY));
|
||||
return config != null && Boolean.parseBoolean(config.getFirst(CONFIG_RECURRING));
|
||||
}
|
||||
|
||||
public boolean isScheduled() {
|
||||
return config != null && Boolean.parseBoolean(config.getFirstOrDefault(SCHEDULED_KEY, "true"));
|
||||
return config != null && Boolean.parseBoolean(config.getFirstOrDefault(CONFIG_SCHEDULED, "true"));
|
||||
}
|
||||
|
||||
public Long getNotBefore() {
|
||||
@ -87,4 +89,18 @@ public class Workflow {
|
||||
public void setNotBefore(Long notBefore) {
|
||||
this.notBefore = notBefore;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (config == null) {
|
||||
config = new MultivaluedHashMap<>();
|
||||
}
|
||||
config.putSingle(CONFIG_ENABLED, String.valueOf(enabled));
|
||||
}
|
||||
|
||||
public void setError(String message) {
|
||||
if (config == null) {
|
||||
config = new MultivaluedHashMap<>();
|
||||
}
|
||||
config.putSingle(CONFIG_ERROR, message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_PRIORITY;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
@ -24,9 +27,6 @@ import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
|
||||
public static final String AFTER_KEY = "after";
|
||||
public static final String PRIORITY_KEY = "priority";
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
@ -72,11 +72,11 @@ public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
setConfig(PRIORITY_KEY, String.valueOf(priority));
|
||||
setConfig(CONFIG_PRIORITY, String.valueOf(priority));
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
String value = getConfig().getFirst(PRIORITY_KEY);
|
||||
String value = getConfig().getFirst(CONFIG_PRIORITY);
|
||||
if (value == null) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
@ -88,11 +88,11 @@ public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
}
|
||||
|
||||
public void setAfter(Long ms) {
|
||||
setConfig(AFTER_KEY, String.valueOf(ms));
|
||||
setConfig(CONFIG_AFTER, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public Long getAfter() {
|
||||
return Long.valueOf(getConfig().getFirstOrDefault(AFTER_KEY, "0"));
|
||||
return Long.valueOf(getConfig().getFirstOrDefault(CONFIG_AFTER, "0"));
|
||||
}
|
||||
|
||||
public List<WorkflowStep> getSteps() {
|
||||
|
||||
@ -10,7 +10,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class AggregatedStepProviderFactory implements WorkflowStepProviderFactory<AggregatedStepProvider> {
|
||||
|
||||
public static final String ID = "aggregated-step-provider";
|
||||
public static final String ID = "aggregated";
|
||||
|
||||
@Override
|
||||
public AggregatedStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
@ -27,7 +27,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DeleteUserStepProviderFactory implements WorkflowStepProviderFactory<DeleteUserStepProvider> {
|
||||
|
||||
public static final String ID = "delete-user-step-provider";
|
||||
public static final String ID = "delete-user";
|
||||
|
||||
@Override
|
||||
public DeleteUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
@ -27,7 +27,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DisableUserStepProviderFactory implements WorkflowStepProviderFactory<DisableUserStepProvider> {
|
||||
|
||||
public static final String ID = "disable-user-step-provider";
|
||||
public static final String ID = "disable-user";
|
||||
|
||||
@Override
|
||||
public DisableUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.email.EmailException;
|
||||
@ -30,8 +32,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.models.workflow.WorkflowStep.AFTER_KEY;
|
||||
|
||||
public class NotifyUserStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private static final String ACCOUNT_DISABLE_NOTIFICATION_SUBJECT = "accountDisableNotificationSubject";
|
||||
@ -153,8 +153,8 @@ public class NotifyUserStepProvider implements WorkflowStepProvider {
|
||||
boolean foundCurrent = false;
|
||||
for (ComponentModel step : steps) {
|
||||
if (foundCurrent) {
|
||||
timeToNextNonNotificationStep += step.get(AFTER_KEY, 0L);
|
||||
if (!step.getProviderId().equals("notify-user-step-provider")) {
|
||||
timeToNextNonNotificationStep += step.get(CONFIG_AFTER, 0L);
|
||||
if (!step.getProviderId().equals("notify-user")) {
|
||||
// we found the next non-notification action, accumulate its time and break
|
||||
return Map.of(step, timeToNextNonNotificationStep);
|
||||
}
|
||||
@ -169,16 +169,16 @@ public class NotifyUserStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private String getDefaultSubjectKey(String stepType) {
|
||||
return switch (stepType) {
|
||||
case "disable-user-step-provider" -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT;
|
||||
case "delete-user-step-provider" -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT;
|
||||
case DisableUserStepProviderFactory.ID -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT;
|
||||
case DeleteUserStepProviderFactory.ID -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT;
|
||||
default -> "accountNotificationSubject";
|
||||
};
|
||||
}
|
||||
|
||||
private String getDefaultMessageKey(String stepType) {
|
||||
return switch (stepType) {
|
||||
case "disable-user-step-provider" -> ACCOUNT_DISABLE_NOTIFICATION_BODY;
|
||||
case "delete-user-step-provider" -> ACCOUNT_DELETE_NOTIFICATION_BODY;
|
||||
case DisableUserStepProviderFactory.ID -> ACCOUNT_DISABLE_NOTIFICATION_BODY;
|
||||
case DeleteUserStepProviderFactory.ID -> ACCOUNT_DELETE_NOTIFICATION_BODY;
|
||||
default -> "accountNotificationBody";
|
||||
};
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class NotifyUserStepProviderFactory implements WorkflowStepProviderFactory<NotifyUserStepProvider> {
|
||||
|
||||
public static final String ID = "notify-user-step-provider";
|
||||
public static final String ID = "notify-user";
|
||||
|
||||
@Override
|
||||
public NotifyUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_PRIORITY;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@ -26,9 +29,6 @@ import org.keycloak.models.UserModel;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import static org.keycloak.models.workflow.WorkflowStep.AFTER_KEY;
|
||||
import static org.keycloak.models.workflow.WorkflowStep.PRIORITY_KEY;
|
||||
|
||||
public class SetUserAttributeStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
@ -55,7 +55,7 @@ public class SetUserAttributeStepProvider implements WorkflowStepProvider {
|
||||
for (Entry<String, List<String>> entry : stepModel.getConfig().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
||||
if (!key.startsWith(AFTER_KEY) && !key.startsWith(PRIORITY_KEY)) {
|
||||
if (!key.startsWith(CONFIG_AFTER) && !key.startsWith(CONFIG_PRIORITY)) {
|
||||
log.debugv("Setting attribute {0} to user {1})", key, user.getId());
|
||||
user.setAttribute(key, entry.getValue());
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class SetUserAttributeStepProviderFactory implements WorkflowStepProviderFactory<SetUserAttributeStepProvider> {
|
||||
|
||||
public static final String ID = "set-user-attr-step-provider";
|
||||
public static final String ID = "set-user-attribute";
|
||||
|
||||
@Override
|
||||
public SetUserAttributeStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
|
||||
@ -25,26 +25,29 @@ import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
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 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.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.models.workflow.Workflow.RECURRING_KEY;
|
||||
import static org.keycloak.models.workflow.Workflow.SCHEDULED_KEY;
|
||||
import static org.keycloak.models.workflow.WorkflowStep.AFTER_KEY;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
public class WorkflowsManager {
|
||||
|
||||
@ -66,12 +69,12 @@ public class WorkflowsManager {
|
||||
return addWorkflow(new Workflow(providerId, config));
|
||||
}
|
||||
|
||||
public Workflow addWorkflow(Workflow workflow) {
|
||||
private Workflow addWorkflow(Workflow workflow) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel model = new ComponentModel();
|
||||
|
||||
model.setParentId(realm.getId());
|
||||
model.setProviderId(workflow.getProviderId());
|
||||
model.setProviderId(ofNullable(workflow.getProviderId()).orElse(WorkflowConstants.DEFAULT_WORKFLOW));
|
||||
model.setProviderType(WorkflowProvider.class.getName());
|
||||
|
||||
MultivaluedHashMap<String, String> config = workflow.getConfig();
|
||||
@ -84,26 +87,22 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
// This method takes an ordered list of steps. First step in the list has the highest priority, last step has the lowest priority
|
||||
public void createSteps(Workflow workflow, List<WorkflowStep> steps) {
|
||||
private void addSteps(Workflow workflow, String parentId, List<WorkflowStep> steps) {
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep step = steps.get(i);
|
||||
|
||||
if (workflow.getId().equals(parentId)) {
|
||||
// only validate top-level steps, sub-steps are validated as part of the parent step validation
|
||||
validateStep(workflow, step);
|
||||
}
|
||||
|
||||
// assign priority based on index.
|
||||
step.setPriority(i + 1);
|
||||
|
||||
List<WorkflowStep> subSteps = Optional.ofNullable(step.getSteps()).orElse(List.of());
|
||||
|
||||
// persist the new step component.
|
||||
step = addStep(workflow.getId(), step);
|
||||
step = addStep(parentId, step);
|
||||
|
||||
for (int j = 0; j < subSteps.size(); j++) {
|
||||
WorkflowStep subStep = subSteps.get(j);
|
||||
// assign priority based on index.
|
||||
subStep.setPriority(j + 1);
|
||||
addStep(step.getId(), subStep);
|
||||
}
|
||||
addSteps(workflow, step.getId(), step.getSteps());
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,9 +182,18 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
public WorkflowStepProvider getStepProvider(WorkflowStep step) {
|
||||
return (WorkflowStepProvider) getStepProviderFactory(step).create(session, getRealm().getComponent(step.getId()));
|
||||
}
|
||||
|
||||
private ComponentFactory<?, ?> getStepProviderFactory(WorkflowStep step) {
|
||||
ComponentFactory<?, ?> stepFactory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(WorkflowStepProvider.class, step.getProviderId());
|
||||
return (WorkflowStepProvider) stepFactory.create(session, getRealm().getComponent(step.getId()));
|
||||
|
||||
if (stepFactory == null) {
|
||||
throw new WorkflowInvalidStateException("Step not found: " + step.getProviderId());
|
||||
}
|
||||
|
||||
return stepFactory;
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
@ -247,8 +255,8 @@ public class WorkflowsManager {
|
||||
}
|
||||
}
|
||||
} catch (WorkflowInvalidStateException e) {
|
||||
workflow.getConfig().putSingle("enabled", "false");
|
||||
workflow.getConfig().putSingle("validation_error", e.getMessage());
|
||||
workflow.setEnabled(false);
|
||||
workflow.setError(e.getMessage());
|
||||
updateWorkflow(workflow, workflow.getConfig());
|
||||
log.debugf("Workflow %s was disabled due to: %s", workflow.getId(), e.getMessage());
|
||||
}
|
||||
@ -319,16 +327,53 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
public WorkflowRepresentation toRepresentation(Workflow workflow) {
|
||||
WorkflowRepresentation rep = new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig());
|
||||
List<WorkflowConditionRepresentation> conditions = toConditionRepresentation(workflow);
|
||||
List<WorkflowStepRepresentation> steps = toRepresentation(getSteps(workflow.getId()));
|
||||
|
||||
for (WorkflowStep step : getSteps(workflow.getId())) {
|
||||
rep.addStep(toRepresentation(step));
|
||||
return new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig(), conditions, steps);
|
||||
}
|
||||
|
||||
return rep;
|
||||
private List<WorkflowConditionRepresentation> toConditionRepresentation(Workflow workflow) {
|
||||
MultivaluedHashMap<String, String> workflowConfig = ofNullable(workflow.getConfig()).orElse(new MultivaluedHashMap<>());
|
||||
List<String> ids = workflowConfig.getOrDefault(CONFIG_CONDITIONS, List.of());
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private WorkflowStepRepresentation toRepresentation(WorkflowStep step) {
|
||||
List<WorkflowConditionRepresentation> conditions = new ArrayList<>();
|
||||
|
||||
for (String id : ids) {
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
|
||||
for (Entry<String, List<String>> 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;
|
||||
}
|
||||
|
||||
private List<WorkflowStepRepresentation> toRepresentation(List<WorkflowStep> existingSteps) {
|
||||
if (existingSteps == null || existingSteps.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<WorkflowStepRepresentation> steps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStep step : existingSteps) {
|
||||
steps.add(toRepresentation(step));
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation toRepresentation(WorkflowStep step) {
|
||||
List<WorkflowStepRepresentation> steps = step.getSteps().stream().map(this::toRepresentation).toList();
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps);
|
||||
}
|
||||
@ -340,22 +385,23 @@ public class WorkflowsManager {
|
||||
validateWorkflow(rep, config);
|
||||
|
||||
for (WorkflowConditionRepresentation condition : conditions) {
|
||||
String conditionProviderId = condition.getProviderId();
|
||||
config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId);
|
||||
String conditionProviderId = condition.getUses();
|
||||
getConditionProviderFactory(conditionProviderId);
|
||||
config.computeIfAbsent(CONFIG_CONDITIONS, key -> new ArrayList<>()).add(conditionProviderId);
|
||||
|
||||
for (Entry<String, List<String>> configEntry : condition.getConfig().entrySet()) {
|
||||
config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Workflow workflow = addWorkflow(rep.getProviderId(), config);
|
||||
Workflow workflow = addWorkflow(rep.getUses(), config);
|
||||
List<WorkflowStep> steps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation stepRep : rep.getSteps()) {
|
||||
steps.add(toModel(stepRep));
|
||||
}
|
||||
|
||||
createSteps(workflow, steps);
|
||||
addSteps(workflow, workflow.getId(), steps);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
@ -366,10 +412,10 @@ public class WorkflowsManager {
|
||||
// immediate workflow cannot have time conditions
|
||||
// all steps of scheduled workflow must have time condition
|
||||
|
||||
boolean isImmediate = config.containsKey(SCHEDULED_KEY) && !Boolean.parseBoolean(config.getFirst(SCHEDULED_KEY));
|
||||
boolean isRecurring = config.containsKey(RECURRING_KEY) && Boolean.parseBoolean(config.getFirst(RECURRING_KEY));
|
||||
boolean isImmediate = config.containsKey(CONFIG_SCHEDULED) && !Boolean.parseBoolean(config.getFirst(CONFIG_SCHEDULED));
|
||||
boolean isRecurring = config.containsKey(CONFIG_RECURRING) && Boolean.parseBoolean(config.getFirst(CONFIG_RECURRING));
|
||||
boolean hasTimeCondition = rep.getSteps().stream().allMatch(step -> step.getConfig() != null
|
||||
&& step.getConfig().containsKey(AFTER_KEY));
|
||||
&& step.getConfig().containsKey(CONFIG_AFTER));
|
||||
if (isImmediate && isRecurring) {
|
||||
throw new WorkflowInvalidStateException("Workflow cannot be both immediate and recurring.");
|
||||
}
|
||||
@ -379,6 +425,20 @@ public class WorkflowsManager {
|
||||
if (!isImmediate && !hasTimeCondition) {
|
||||
throw new WorkflowInvalidStateException("Scheduled workflow cannot have steps without time conditions.");
|
||||
}
|
||||
|
||||
validateEvents(rep.getOnValues());
|
||||
validateEvents(rep.getOnEventsReset());
|
||||
}
|
||||
|
||||
|
||||
private static void validateEvents(List<String> 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 WorkflowStep toModel(WorkflowStepRepresentation rep) {
|
||||
@ -388,7 +448,7 @@ public class WorkflowsManager {
|
||||
subSteps.add(toModel(subStep));
|
||||
}
|
||||
|
||||
return new WorkflowStep(rep.getProviderId(), rep.getConfig(), subSteps);
|
||||
return new WorkflowStep(rep.getUses(), rep.getConfig(), subSteps);
|
||||
}
|
||||
|
||||
public void bind(Workflow workflow, ResourceType type, String resourceId) {
|
||||
@ -405,6 +465,8 @@ public class WorkflowsManager {
|
||||
boolean isAggregatedStep = !step.getSteps().isEmpty();
|
||||
boolean isScheduledWorkflow = workflow.isScheduled();
|
||||
|
||||
getStepProviderFactory(step);
|
||||
|
||||
if (isAggregatedStep) {
|
||||
if (!step.getProviderId().equals(AggregatedStepProviderFactory.ID)) {
|
||||
// for now, only AggregatedStepProvider supports having sub-steps, but we might want to support
|
||||
@ -417,17 +479,17 @@ public class WorkflowsManager {
|
||||
// for each sub-step (in case it's not aggregated step on its own) check all it's sub-steps do not have
|
||||
// time conditions, all its sub-steps are meant to be run at once
|
||||
if (subSteps.stream().anyMatch(subStep ->
|
||||
subStep.getConfig().getFirst(AFTER_KEY) != null &&
|
||||
subStep.getConfig().getFirst(CONFIG_AFTER) != null &&
|
||||
!subStep.getProviderId().equals(AggregatedStepProviderFactory.ID))) {
|
||||
throw new ModelValidationException("Sub-steps of aggregated step cannot have time conditions.");
|
||||
}
|
||||
} else {
|
||||
if (isScheduledWorkflow) {
|
||||
if (step.getConfig().getFirst(AFTER_KEY) == null || step.getAfter() < 0) {
|
||||
if (step.getConfig().getFirst(CONFIG_AFTER) == null || step.getAfter() < 0) {
|
||||
throw new ModelValidationException("All steps of scheduled workflow must have a valid 'after' time condition.");
|
||||
}
|
||||
} else { // immediate workflow
|
||||
if (step.getConfig().getFirst(AFTER_KEY) != null) {
|
||||
if (step.getConfig().getFirst(CONFIG_AFTER) != null) {
|
||||
throw new ModelValidationException("Immediate workflow step cannot have a time condition.");
|
||||
}
|
||||
}
|
||||
@ -524,13 +586,6 @@ public class WorkflowsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation toStepRepresentation(WorkflowStep step) {
|
||||
List<WorkflowStepRepresentation> steps = step.getSteps().stream()
|
||||
.map(this::toStepRepresentation)
|
||||
.toList();
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps);
|
||||
}
|
||||
|
||||
public WorkflowStep toStepModel(WorkflowStepRepresentation rep) {
|
||||
List<WorkflowStep> subSteps = new ArrayList<>();
|
||||
|
||||
@ -538,6 +593,36 @@ public class WorkflowsManager {
|
||||
subSteps.add(toStepModel(subStep));
|
||||
}
|
||||
|
||||
return new WorkflowStep(rep.getProviderId(), rep.getConfig(), subSteps);
|
||||
return new WorkflowStep(rep.getUses(), rep.getConfig(), subSteps);
|
||||
}
|
||||
|
||||
public WorkflowConditionProvider getConditionProvider(String providerId, MultivaluedHashMap<String, String> modelConfig) {
|
||||
WorkflowConditionProviderFactory<WorkflowConditionProvider> providerFactory = getConditionProviderFactory(providerId);
|
||||
Map<String, List<String>> config = new HashMap<>();
|
||||
|
||||
for (Entry<String, List<String>> configEntry : modelConfig.entrySet()) {
|
||||
if (configEntry.getKey().startsWith(providerId)) {
|
||||
config.put(configEntry.getKey().substring(providerId.length() + 1), configEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
WorkflowConditionProvider condition = providerFactory.create(session, config);
|
||||
|
||||
if (condition == null) {
|
||||
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
private WorkflowConditionProviderFactory<WorkflowConditionProvider> getConditionProviderFactory(String providerId) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
WorkflowConditionProviderFactory<WorkflowConditionProvider> providerFactory = (WorkflowConditionProviderFactory<WorkflowConditionProvider>) sessionFactory.getProviderFactory(WorkflowConditionProvider.class, providerId);
|
||||
|
||||
if (providerFactory == null) {
|
||||
throw new WorkflowInvalidStateException("Could not find condition provider: " + providerId);
|
||||
}
|
||||
|
||||
return providerFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ public class WorkflowStepsResource {
|
||||
})
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
return workflowsManager.getSteps(workflow.getId()).stream()
|
||||
.map(workflowsManager::toStepRepresentation)
|
||||
.map(workflowsManager::toRepresentation)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@ -111,8 +111,7 @@ public class WorkflowStepsResource {
|
||||
WorkflowStep step = workflowsManager.toStepModel(stepRep);
|
||||
WorkflowStep addedStep = workflowsManager.addStepToWorkflow(workflow.getId(), step, position);
|
||||
|
||||
WorkflowStepRepresentation result = workflowsManager.toStepRepresentation(addedStep);
|
||||
return Response.ok(result).build();
|
||||
return Response.ok(workflowsManager.toRepresentation(addedStep)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,10 +164,11 @@ public class WorkflowStepsResource {
|
||||
}
|
||||
|
||||
WorkflowStep step = workflowsManager.getStepById(stepId);
|
||||
|
||||
if (step == null) {
|
||||
throw new BadRequestException("Step not found: " + stepId);
|
||||
}
|
||||
|
||||
return workflowsManager.toStepRepresentation(step);
|
||||
return workflowsManager.toRepresentation(step);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,9 +16,11 @@ import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowSetRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class WorkflowsResource {
|
||||
|
||||
@ -44,10 +46,11 @@ public class WorkflowsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@Path("set")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response createAll(List<WorkflowRepresentation> reps) {
|
||||
for (WorkflowRepresentation workflow : reps) {
|
||||
public Response createAll(WorkflowSetRepresentation workflows) {
|
||||
for (WorkflowRepresentation workflow : Optional.ofNullable(workflows.getWorkflows()).orElse(List.of())) {
|
||||
create(workflow).close();
|
||||
}
|
||||
return Response.created(session.getContext().getUri().getRequestUri()).build();
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
@ -61,7 +60,7 @@ public class AdhocWorkflowTest {
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
assertThat(workflow.getSteps(), hasSize(1));
|
||||
WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0);
|
||||
assertThat(aggregatedStep.getProviderId(), is(SetUserAttributeStepProviderFactory.ID));
|
||||
assertThat(aggregatedStep.getUses(), is(SetUserAttributeStepProviderFactory.ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -70,7 +70,7 @@ public class AggregatedStepTest {
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
assertThat(workflow.getSteps(), hasSize(1));
|
||||
WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0);
|
||||
assertThat(aggregatedStep.getProviderId(), is(AggregatedStepProviderFactory.ID));
|
||||
assertThat(aggregatedStep.getUses(), is(AggregatedStepProviderFactory.ID));
|
||||
List<WorkflowStepRepresentation> steps = aggregatedStep.getSteps();
|
||||
assertThat(steps, hasSize(2));
|
||||
assertStep(steps, SetUserAttributeStepProviderFactory.ID, a -> {
|
||||
@ -219,7 +219,7 @@ public class AggregatedStepTest {
|
||||
private void assertStep(List<WorkflowStepRepresentation> steps, String expectedProviderId, Consumer<WorkflowStepRepresentation> assertions) {
|
||||
assertTrue(steps.stream()
|
||||
.anyMatch(a -> {
|
||||
if (a.getProviderId().equals(expectedProviderId)) {
|
||||
if (a.getUses().equals(expectedProviderId)) {
|
||||
assertions.accept(a);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
@ -50,6 +51,7 @@ import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowCondition
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
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;
|
||||
@ -152,11 +154,11 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest {
|
||||
|
||||
// check the workflow is disabled
|
||||
workflowRep = consumerRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation();
|
||||
assertThat(workflowRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false")));
|
||||
List<String> validationErrors = workflowRep.getConfig().get("validation_error");
|
||||
assertThat(validationErrors, notNullValue());
|
||||
assertThat(validationErrors, hasSize(1));
|
||||
assertThat(validationErrors.get(0), containsString("Identity provider %s does not exist.".formatted(IDP_OIDC_ALIAS)));
|
||||
assertThat(workflowRep.getEnabled(), allOf(notNullValue(), is(false)));
|
||||
WorkflowStateRepresentation status = workflowRep.getState();
|
||||
assertThat(status, notNullValue());
|
||||
assertThat(status.getErrors(), hasSize(1));
|
||||
assertThat(status.getErrors().get(0), containsString("Identity provider %s does not exist.".formatted(IDP_OIDC_ALIAS)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -230,6 +232,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Disabled("For now deactivation events is not enabled to any event")
|
||||
@Test
|
||||
public void testAddRemoveFedIdentityAffectsWorkflowAssociation() {
|
||||
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
|
||||
@ -28,6 +28,8 @@ import org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionF
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
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;
|
||||
@ -66,7 +68,7 @@ public class GroupMembershipJoinWorkflowTest {
|
||||
groupId = ApiUtil.getCreatedId(response);
|
||||
}
|
||||
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_ADD.name())
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
@ -151,11 +153,11 @@ public class GroupMembershipJoinWorkflowTest {
|
||||
|
||||
// check the workflow is disabled
|
||||
workflowRep = managedRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation();
|
||||
assertThat(workflowRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false")));
|
||||
List<String> validationErrors = workflowRep.getConfig().get("validation_error");
|
||||
assertThat(validationErrors, notNullValue());
|
||||
assertThat(validationErrors, hasSize(1));
|
||||
assertThat(validationErrors.get(0), containsString("Group with id %s does not exist.".formatted(groupId)));
|
||||
assertThat(workflowRep.getEnabled(), allOf(notNullValue(), is(false)));
|
||||
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)));
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
|
||||
@ -28,6 +28,7 @@ import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory;
|
||||
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;
|
||||
@ -136,7 +137,7 @@ public class RoleWorkflowConditionTest {
|
||||
createRoleIfNotExists(roleName);
|
||||
}
|
||||
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.USER_ROLE_ADD.name())
|
||||
.recurring()
|
||||
|
||||
@ -25,6 +25,7 @@ import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
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;
|
||||
@ -131,7 +132,7 @@ public class UserAttributeWorkflowConditionTest {
|
||||
}
|
||||
|
||||
private void createWorkflow(Map<String, List<String>> attributes) {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.USER_ADD.name())
|
||||
.recurring()
|
||||
|
||||
@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@ -62,6 +63,8 @@ import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
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;
|
||||
@ -93,7 +96,7 @@ public class WorkflowManagementTest {
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
@ -113,15 +116,16 @@ public class WorkflowManagementTest {
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(1));
|
||||
|
||||
assertThat(actualWorkflows.get(0).getProviderId(), is(UserCreationTimeWorkflowProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getUses(), is(UserCreationTimeWorkflowProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getSteps(), Matchers.hasSize(2));
|
||||
assertThat(actualWorkflows.get(0).getSteps().get(0).getProviderId(), is(NotifyUserStepProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getSteps().get(1).getProviderId(), is(DisableUserStepProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getSteps().get(0).getUses(), is(NotifyUserStepProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getSteps().get(1).getUses(), is(DisableUserStepProviderFactory.ID));
|
||||
assertThat(actualWorkflows.get(0).getState(), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWithNoConditions() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
@ -132,13 +136,36 @@ public class WorkflowManagementTest {
|
||||
.build()
|
||||
).build();
|
||||
|
||||
expectedWorkflows.get(0).setConditions(null);
|
||||
expectedWorkflows.getWorkflows().get(0).setConditions(null);
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWithNoWorkflowSetDefaultWorkflow() {
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(null)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
expectedWorkflows.getWorkflows().get(0).setConditions(null);
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
assertEquals(1, managedRealm.admin().workflows().list().size());
|
||||
assertEquals(WorkflowConstants.DEFAULT_WORKFLOW, managedRealm.admin().workflows().list().get(0).getUses());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
@ -166,7 +193,7 @@ public class WorkflowManagementTest {
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(2));
|
||||
|
||||
WorkflowRepresentation workflow = actualWorkflows.stream().filter(p -> UserCreationTimeWorkflowProviderFactory.ID.equals(p.getProviderId())).findAny().orElse(null);
|
||||
WorkflowRepresentation workflow = actualWorkflows.stream().filter(p -> UserCreationTimeWorkflowProviderFactory.ID.equals(p.getUses())).findAny().orElse(null);
|
||||
assertThat(workflow, notNullValue());
|
||||
String id = workflow.getId();
|
||||
workflows.workflow(id).delete().close();
|
||||
@ -187,7 +214,7 @@ public class WorkflowManagementTest {
|
||||
|
||||
@Test
|
||||
public void testUpdate() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.name("test-workflow")
|
||||
.withSteps(
|
||||
@ -438,7 +465,7 @@ public class WorkflowManagementTest {
|
||||
mailServer.runCleanup();
|
||||
|
||||
// disable the workflow - scheduled steps should be paused and workflow should not activate for new users
|
||||
workflow.getConfig().putSingle("enabled", "false");
|
||||
workflow.setEnabled(false);
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close();
|
||||
|
||||
// create another user - should NOT bind the user to the workflow as it is disabled
|
||||
@ -583,7 +610,7 @@ public class WorkflowManagementTest {
|
||||
|
||||
@Test
|
||||
public void testCreateImmediateWorkflowWithTimeConditions() {
|
||||
List<WorkflowRepresentation> workflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
@ -604,7 +631,7 @@ public class WorkflowManagementTest {
|
||||
|
||||
@Test
|
||||
public void testCreateScheduledWorkflowWithoutTimeConditions() {
|
||||
List<WorkflowRepresentation> workflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
@ -622,7 +649,7 @@ public class WorkflowManagementTest {
|
||||
|
||||
@Test
|
||||
public void testCreateWorkflowMarkedAsBothImmediateAndRecurring() {
|
||||
List<WorkflowRepresentation> workflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.recurring()
|
||||
|
||||
@ -34,6 +34,7 @@ import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowSetRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
@ -68,7 +69,7 @@ public class WorkflowStepManagementTest {
|
||||
workflowsResource = managedRealm.admin().workflows();
|
||||
|
||||
// Create a workflow for testing (need at least one step for persistence)
|
||||
List<WorkflowRepresentation> workflows = WorkflowRepresentation.create()
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.USER_ADD.toString())
|
||||
.name("Test Workflow")
|
||||
@ -101,7 +102,7 @@ public class WorkflowStepManagementTest {
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
WorkflowStepRepresentation stepRep = new WorkflowStepRepresentation();
|
||||
stepRep.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
stepRep.setUses(DisableUserStepProviderFactory.ID);
|
||||
stepRep.setConfig("name", "Test Step");
|
||||
stepRep.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
@ -111,7 +112,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
assertNotNull(addedStep);
|
||||
assertNotNull(addedStep.getId());
|
||||
assertEquals(DisableUserStepProviderFactory.ID, addedStep.getProviderId());
|
||||
assertEquals(DisableUserStepProviderFactory.ID, addedStep.getUses());
|
||||
}
|
||||
|
||||
// Verify step is in workflow (should be 2 total: setup step + our added step)
|
||||
@ -120,7 +121,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
// Verify our added step is present
|
||||
boolean foundOurStep = allSteps.stream()
|
||||
.anyMatch(step -> DisableUserStepProviderFactory.ID.equals(step.getProviderId()) &&
|
||||
.anyMatch(step -> DisableUserStepProviderFactory.ID.equals(step.getUses()) &&
|
||||
"Test Step".equals(step.getConfig().getFirst("name")));
|
||||
assertTrue(foundOurStep, "Our added step should be present in the workflow");
|
||||
}
|
||||
@ -132,7 +133,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
// Add one more step
|
||||
WorkflowStepRepresentation step1 = new WorkflowStepRepresentation();
|
||||
step1.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
step1.setUses(DisableUserStepProviderFactory.ID);
|
||||
step1.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
String step1Id;
|
||||
@ -162,7 +163,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
// Add first step at position 0
|
||||
WorkflowStepRepresentation step1 = new WorkflowStepRepresentation();
|
||||
step1.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
step1.setUses(NotifyUserStepProviderFactory.ID);
|
||||
step1.setConfig("name", "Step 1");
|
||||
step1.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
@ -178,7 +179,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
// Add second step at position 1
|
||||
WorkflowStepRepresentation step2 = new WorkflowStepRepresentation();
|
||||
step2.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
step2.setUses(DisableUserStepProviderFactory.ID);
|
||||
step2.setConfig("name", "Step 2");
|
||||
step2.setConfig("after", String.valueOf(Duration.ofDays(60).toMillis()));
|
||||
|
||||
@ -194,7 +195,7 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
// Add third step at position 1 (middle)
|
||||
WorkflowStepRepresentation step3 = new WorkflowStepRepresentation();
|
||||
step3.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
step3.setUses(NotifyUserStepProviderFactory.ID);
|
||||
step3.setConfig("name", "Step 3");
|
||||
step3.setConfig("after", String.valueOf(Duration.ofDays(45).toMillis())); // Between 30 and 60 days
|
||||
|
||||
@ -217,7 +218,7 @@ public class WorkflowStepManagementTest {
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
WorkflowStepRepresentation stepRep = new WorkflowStepRepresentation();
|
||||
stepRep.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
stepRep.setUses(NotifyUserStepProviderFactory.ID);
|
||||
stepRep.setConfig("name", "Test Step");
|
||||
stepRep.setConfig("after", String.valueOf(Duration.ofDays(15).toMillis()));
|
||||
|
||||
@ -231,7 +232,7 @@ public class WorkflowStepManagementTest {
|
||||
WorkflowStepRepresentation retrievedStep = steps.get(stepId);
|
||||
assertNotNull(retrievedStep);
|
||||
assertEquals(stepId, retrievedStep.getId());
|
||||
assertEquals(NotifyUserStepProviderFactory.ID, retrievedStep.getProviderId());
|
||||
assertEquals(NotifyUserStepProviderFactory.ID, retrievedStep.getUses());
|
||||
assertEquals("Test Step", retrievedStep.getConfig().getFirst("name"));
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user