mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
Rename RLM to Workflows
Closes #42512 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
@@ -140,7 +140,7 @@ public class Profile {
|
||||
ROLLING_UPDATES_V1("Rolling Updates", Type.DEFAULT, 1),
|
||||
ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.PREVIEW, 2),
|
||||
|
||||
RESOURCE_LIFECYCLE("Resource lifecycle management", Type.EXPERIMENTAL),
|
||||
WORKFLOWS("Workflows", Type.EXPERIMENTAL),
|
||||
|
||||
LOG_MDC("Mapped Diagnostic Context (MDC) information in logs", Type.PREVIEW),
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.keycloak.representations.resources.policies;
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourcePolicyConditionRepresentation {
|
||||
public class WorkflowConditionRepresentation {
|
||||
|
||||
public static Builder create() {
|
||||
return new Builder();
|
||||
@@ -15,19 +15,19 @@ public class ResourcePolicyConditionRepresentation {
|
||||
private String providerId;
|
||||
private Map<String, List<String>> config;
|
||||
|
||||
public ResourcePolicyConditionRepresentation() {
|
||||
public WorkflowConditionRepresentation() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourcePolicyConditionRepresentation(String providerId) {
|
||||
public WorkflowConditionRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
}
|
||||
|
||||
public ResourcePolicyConditionRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
public WorkflowConditionRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
this(null, providerId, config);
|
||||
}
|
||||
|
||||
public ResourcePolicyConditionRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
public WorkflowConditionRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
@@ -65,10 +65,10 @@ public class ResourcePolicyConditionRepresentation {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ResourcePolicyConditionRepresentation action;
|
||||
private WorkflowConditionRepresentation action;
|
||||
|
||||
public Builder of(String providerId) {
|
||||
this.action = new ResourcePolicyConditionRepresentation(providerId);
|
||||
this.action = new WorkflowConditionRepresentation(providerId);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public class ResourcePolicyConditionRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResourcePolicyConditionRepresentation build() {
|
||||
public WorkflowConditionRepresentation build() {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.representations.resources.policies;
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -11,7 +11,7 @@ import java.util.Optional;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
public class ResourcePolicyRepresentation {
|
||||
public class WorkflowRepresentation {
|
||||
|
||||
public static Builder create() {
|
||||
return new Builder();
|
||||
@@ -20,22 +20,22 @@ public class ResourcePolicyRepresentation {
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<ResourcePolicyActionRepresentation> actions;
|
||||
private List<ResourcePolicyConditionRepresentation> conditions;
|
||||
private List<WorkflowStepRepresentation> steps;
|
||||
private List<WorkflowConditionRepresentation> conditions;
|
||||
|
||||
public ResourcePolicyRepresentation() {
|
||||
public WorkflowRepresentation() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourcePolicyRepresentation(String providerId) {
|
||||
public WorkflowRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
}
|
||||
|
||||
public ResourcePolicyRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
public WorkflowRepresentation(String providerId, Map<String, List<String>> config) {
|
||||
this(null, providerId, config);
|
||||
}
|
||||
|
||||
public ResourcePolicyRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
public WorkflowRepresentation(String id, String providerId, Map<String, List<String>> config) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = new MultivaluedHashMap<>(config);
|
||||
@@ -68,38 +68,38 @@ public class ResourcePolicyRepresentation {
|
||||
this.config.putSingle("name", name);
|
||||
}
|
||||
|
||||
public void setConditions(List<ResourcePolicyConditionRepresentation> conditions) {
|
||||
public void setConditions(List<WorkflowConditionRepresentation> conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public List<ResourcePolicyConditionRepresentation> getConditions() {
|
||||
public List<WorkflowConditionRepresentation> getConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
public void setActions(List<ResourcePolicyActionRepresentation> actions) {
|
||||
this.actions = actions;
|
||||
public void setSteps(List<WorkflowStepRepresentation> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public List<ResourcePolicyActionRepresentation> getActions() {
|
||||
return actions;
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void addAction(ResourcePolicyActionRepresentation action) {
|
||||
if (actions == null) {
|
||||
actions = new ArrayList<>();
|
||||
public void addStep(WorkflowStepRepresentation step) {
|
||||
if (steps == null) {
|
||||
steps = new ArrayList<>();
|
||||
}
|
||||
actions.add(action);
|
||||
steps.add(step);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String providerId;
|
||||
private Map<String, List<String>> config = new HashMap<>();
|
||||
private List<ResourcePolicyConditionRepresentation> conditions = new ArrayList<>();
|
||||
private final Map<String, List<ResourcePolicyActionRepresentation>> actions = new HashMap<>();
|
||||
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 List<Builder> builders = new ArrayList<>();
|
||||
|
||||
private Builder() {
|
||||
@@ -124,7 +124,7 @@ public class ResourcePolicyRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onConditions(ResourcePolicyConditionRepresentation... condition) {
|
||||
public Builder onConditions(WorkflowConditionRepresentation... condition) {
|
||||
if (conditions == null) {
|
||||
conditions = new ArrayList<>();
|
||||
}
|
||||
@@ -132,8 +132,8 @@ public class ResourcePolicyRepresentation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withActions(ResourcePolicyActionRepresentation... actions) {
|
||||
this.actions.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(actions));
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
this.steps.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(steps));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -159,21 +159,21 @@ public class ResourcePolicyRepresentation {
|
||||
return withConfig("recurring", "true");
|
||||
}
|
||||
|
||||
public List<ResourcePolicyRepresentation> build() {
|
||||
List<ResourcePolicyRepresentation> policies = new ArrayList<>();
|
||||
public List<WorkflowRepresentation> build() {
|
||||
List<WorkflowRepresentation> workflows = new ArrayList<>();
|
||||
|
||||
for (Builder builder : builders) {
|
||||
for (Entry<String, List<ResourcePolicyActionRepresentation>> entry : builder.actions.entrySet()) {
|
||||
ResourcePolicyRepresentation policy = new ResourcePolicyRepresentation(entry.getKey(), builder.config);
|
||||
for (Entry<String, List<WorkflowStepRepresentation>> entry : builder.steps.entrySet()) {
|
||||
WorkflowRepresentation workflow = new WorkflowRepresentation(entry.getKey(), builder.config);
|
||||
|
||||
policy.setActions(entry.getValue());
|
||||
policy.setConditions(builder.conditions);
|
||||
workflow.setSteps(entry.getValue());
|
||||
workflow.setConditions(builder.conditions);
|
||||
|
||||
policies.add(policy);
|
||||
workflows.add(workflow);
|
||||
}
|
||||
}
|
||||
|
||||
return policies;
|
||||
return workflows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.representations.resources.policies;
|
||||
package org.keycloak.representations.workflows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
@@ -7,7 +7,7 @@ import java.util.List;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
public class ResourcePolicyActionRepresentation {
|
||||
public class WorkflowStepRepresentation {
|
||||
|
||||
private static final String AFTER_KEY = "after";
|
||||
|
||||
@@ -18,25 +18,25 @@ public class ResourcePolicyActionRepresentation {
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<ResourcePolicyActionRepresentation> actions;
|
||||
private List<WorkflowStepRepresentation> steps;
|
||||
|
||||
public ResourcePolicyActionRepresentation() {
|
||||
public WorkflowStepRepresentation() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourcePolicyActionRepresentation(String providerId) {
|
||||
public WorkflowStepRepresentation(String providerId) {
|
||||
this(providerId, null);
|
||||
}
|
||||
|
||||
public ResourcePolicyActionRepresentation(String providerId, MultivaluedHashMap<String, String> config) {
|
||||
public WorkflowStepRepresentation(String providerId, MultivaluedHashMap<String, String> config) {
|
||||
this(null, providerId, config, null);
|
||||
}
|
||||
|
||||
public ResourcePolicyActionRepresentation(String id, String providerId, MultivaluedHashMap<String, String> config, List<ResourcePolicyActionRepresentation> actions) {
|
||||
public WorkflowStepRepresentation(String id, String providerId, MultivaluedHashMap<String, String> config, List<WorkflowStepRepresentation> steps) {
|
||||
this.id = id;
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
this.actions = actions;
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
@@ -74,54 +74,54 @@ public class ResourcePolicyActionRepresentation {
|
||||
setConfig(AFTER_KEY, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public List<ResourcePolicyActionRepresentation> getActions() {
|
||||
return actions;
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public void setActions(List<ResourcePolicyActionRepresentation> actions) {
|
||||
this.actions = actions;
|
||||
public void setSteps(List<WorkflowStepRepresentation> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ResourcePolicyActionRepresentation action;
|
||||
private WorkflowStepRepresentation step;
|
||||
|
||||
public Builder of(String providerId) {
|
||||
this.action = new ResourcePolicyActionRepresentation(providerId);
|
||||
this.step = new WorkflowStepRepresentation(providerId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder after(Duration duration) {
|
||||
action.setAfter(duration.toMillis());
|
||||
step.setAfter(duration.toMillis());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder before(ResourcePolicyActionRepresentation targetAction, Duration timeBeforeTarget) {
|
||||
// Calculate absolute time: targetAction.after - timeBeforeTarget
|
||||
String targetAfter = targetAction.getConfig().get(AFTER_KEY).get(0);
|
||||
public Builder before(WorkflowStepRepresentation targetStep, Duration timeBeforeTarget) {
|
||||
// Calculate absolute time: targetStep.after - timeBeforeTarget
|
||||
String targetAfter = targetStep.getConfig().get(AFTER_KEY).get(0);
|
||||
long targetTime = Long.parseLong(targetAfter);
|
||||
long thisTime = targetTime - timeBeforeTarget.toMillis();
|
||||
action.setAfter(thisTime);
|
||||
step.setAfter(thisTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, String value) {
|
||||
action.setConfig(key, value);
|
||||
step.setConfig(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withActions(ResourcePolicyActionRepresentation... actions) {
|
||||
action.setActions(Arrays.asList(actions));
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
step.setSteps(Arrays.asList(steps));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withConfig(String key, List<String> values) {
|
||||
action.setConfig(key, values);
|
||||
step.setConfig(key, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResourcePolicyActionRepresentation build() {
|
||||
return action;
|
||||
public WorkflowStepRepresentation build() {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,6 +421,6 @@ public interface RealmResource {
|
||||
@Path("client-types")
|
||||
ClientTypesResource clientTypes();
|
||||
|
||||
@Path("resources")
|
||||
RealmResourcesResource resources();
|
||||
@Path("workflows")
|
||||
WorkflowsResource workflows();
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.keycloak.admin.client.resource;
|
||||
|
||||
import jakarta.ws.rs.Path;
|
||||
|
||||
public interface RealmResourcesResource {
|
||||
|
||||
@Path("policies")
|
||||
RealmResourcePolicies policies();
|
||||
}
|
||||
@@ -12,20 +12,20 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
public interface RealmResourcePolicy {
|
||||
public interface WorkflowResource {
|
||||
|
||||
@DELETE
|
||||
Response delete();
|
||||
|
||||
@PUT
|
||||
@Consumes(APPLICATION_JSON)
|
||||
Response update(ResourcePolicyRepresentation policy);
|
||||
Response update(WorkflowRepresentation workflow);
|
||||
|
||||
@GET
|
||||
@Produces(APPLICATION_JSON)
|
||||
ResourcePolicyRepresentation toRepresentation();
|
||||
WorkflowRepresentation toRepresentation();
|
||||
|
||||
@Path("bind/{type}/{resourceId}")
|
||||
@POST
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -10,22 +8,24 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
public interface RealmResourcePolicies {
|
||||
import java.util.List;
|
||||
|
||||
public interface WorkflowsResource {
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response create(ResourcePolicyRepresentation representation);
|
||||
Response create(WorkflowRepresentation representation);
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response create(List<ResourcePolicyRepresentation> representation);
|
||||
Response create(List<WorkflowRepresentation> representation);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<ResourcePolicyRepresentation> list();
|
||||
List<WorkflowRepresentation> list();
|
||||
|
||||
@Path("{id}")
|
||||
RealmResourcePolicy policy(@PathParam("id") String id);
|
||||
WorkflowResource workflow(@PathParam("id") String id);
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import org.keycloak.models.SubjectCredentialManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.cache.CachedUserModel;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedUser;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaDelete;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvider {
|
||||
|
||||
private final EntityManager em;
|
||||
private static final Logger LOGGER = Logger.getLogger(JpaResourcePolicyStateProvider.class);
|
||||
private final KeycloakSession session;
|
||||
|
||||
public JpaResourcePolicyStateProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledAction getScheduledAction(String policyId, String resourceId) {
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policyId);
|
||||
ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
if (entity != null) {
|
||||
return new ScheduledAction(entity.getPolicyId(), entity.getScheduledActionId(), entity.getResourceId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleAction(ResourcePolicy policy, ResourceAction action, String resourceId) {
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policy.getId());
|
||||
ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
if (entity == null) {
|
||||
entity = new ResourcePolicyStateEntity();
|
||||
entity.setResourceId(resourceId);
|
||||
entity.setPolicyId(policy.getId());
|
||||
entity.setPolicyProviderId(policy.getProviderId());
|
||||
entity.setScheduledActionId(action.getId());
|
||||
entity.setScheduledActionTimestamp(Time.currentTimeMillis() + action.getAfter());
|
||||
em.persist(entity);
|
||||
}
|
||||
else {
|
||||
entity.setScheduledActionId(action.getId());
|
||||
entity.setScheduledActionTimestamp(Time.currentTimeMillis() + action.getAfter());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledAction> getDueScheduledActions(ResourcePolicy policy) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate byPolicy = cb.equal(stateRoot.get("policyId"), policy.getId());
|
||||
Predicate isExpired = cb.lessThan(stateRoot.get("scheduledActionTimestamp"), Time.currentTimeMillis());
|
||||
|
||||
query.where(cb.and(byPolicy, isExpired));
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledAction> getScheduledActionsByPolicy(String id) {
|
||||
if (StringUtil.isBlank(id)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate byPolicy = cb.equal(stateRoot.get("policyId"), id);
|
||||
query.where(byPolicy);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledAction> getScheduledActionsByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate byResource = cb.equal(stateRoot.get("resourceId"), resourceId);
|
||||
query.where(byResource);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> root = delete.from(ResourcePolicyStateEntity.class);
|
||||
delete.where(cb.equal(root.get("resourceId"), resourceId));
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
LOGGER.tracev("Deleted {0} orphaned state records for resource {1}", deletedCount, resourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String policyId, String resourceId) {
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policyId);
|
||||
ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
if (entity != null) {
|
||||
em.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String policyId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> root = delete.from(ResourcePolicyStateEntity.class);
|
||||
delete.where(cb.equal(root.get("policyId"), policyId));
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -32,31 +32,31 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
public abstract class AbstractUserResourcePolicyProvider extends EventBasedResourcePolicyProvider {
|
||||
public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowProvider {
|
||||
|
||||
private final EntityManager em;
|
||||
|
||||
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
public AbstractUserWorkflowProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getEligibleResourcesForInitialAction() {
|
||||
public List<String> getEligibleResourcesForInitialStep() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<UserEntity> userRoot = query.from(UserEntity.class);
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
// Subquery will find if a state record exists for the user and policy
|
||||
// SELECT 1 FROM ResourcePolicyStateEntity s WHERE s.resourceId = userRoot.id AND s.policyId = :policyId
|
||||
// Subquery will find if a state record exists for the user and workflow
|
||||
// SELECT 1 FROM WorkflowActionStateEntity s WHERE s.resourceId = userRoot.id AND s.workflowId = :workflowId
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = subquery.from(ResourcePolicyStateEntity.class);
|
||||
Root<WorkflowStateEntity> stateRoot = subquery.from(WorkflowStateEntity.class);
|
||||
subquery.select(cb.literal(1));
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(stateRoot.get("resourceId"), userRoot.get("id")),
|
||||
cb.equal(stateRoot.get("policyId"), getModel().getId())
|
||||
cb.equal(stateRoot.get("workflowId"), getModel().getId())
|
||||
)
|
||||
);
|
||||
Predicate notExistsPredicate = cb.not(cb.exists(subquery));
|
||||
@@ -79,7 +79,7 @@ public abstract class AbstractUserResourcePolicyProvider extends EventBasedResou
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
for (String providerId : conditions) {
|
||||
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
|
||||
WorkflowConditionProvider condition = resolveCondition(providerId);
|
||||
Predicate predicate = condition.toPredicate(cb, query, path);
|
||||
|
||||
if (predicate != null) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -9,18 +9,18 @@ import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider {
|
||||
public class EventBasedWorkflowProvider implements WorkflowProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel model;
|
||||
|
||||
public EventBasedResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
public EventBasedWorkflowProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getEligibleResourcesForInitialAction() {
|
||||
public List<String> getEligibleResourcesForInitialStep() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean activateOnEvent(ResourcePolicyEvent event) {
|
||||
public boolean activateOnEvent(WorkflowEvent event) {
|
||||
if (!supports(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
return evaluate(event);
|
||||
}
|
||||
|
||||
protected boolean isActivationEvent(ResourcePolicyEvent event) {
|
||||
protected boolean isActivationEvent(WorkflowEvent event) {
|
||||
ResourceOperationType operation = event.getOperation();
|
||||
|
||||
if (ResourceOperationType.AD_HOC.equals(operation)) {
|
||||
@@ -55,7 +55,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deactivateOnEvent(ResourcePolicyEvent event) {
|
||||
public boolean deactivateOnEvent(WorkflowEvent event) {
|
||||
if (!supports(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -74,11 +74,11 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resetOnEvent(ResourcePolicyEvent event) {
|
||||
public boolean resetOnEvent(WorkflowEvent event) {
|
||||
return isResetEvent(event) && evaluate(event);
|
||||
}
|
||||
|
||||
protected boolean isResetEvent(ResourcePolicyEvent event) {
|
||||
protected boolean isResetEvent(WorkflowEvent event) {
|
||||
boolean resetEventEnabled = Boolean.parseBoolean(getModel().getConfig().getFirstOrDefault("reset-event-enabled", Boolean.FALSE.toString()));
|
||||
return resetEventEnabled && isActivationEvent(event);
|
||||
}
|
||||
@@ -88,11 +88,11 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
|
||||
}
|
||||
|
||||
protected boolean evaluate(ResourcePolicyEvent event) {
|
||||
protected boolean evaluate(WorkflowEvent event) {
|
||||
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
|
||||
|
||||
for (String providerId : conditions) {
|
||||
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
|
||||
WorkflowConditionProvider condition = resolveCondition(providerId);
|
||||
|
||||
if (!condition.evaluate(event)) {
|
||||
return false;
|
||||
@@ -102,9 +102,9 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
return true;
|
||||
}
|
||||
|
||||
protected ResourcePolicyConditionProvider resolveCondition(String providerId) {
|
||||
protected WorkflowConditionProvider resolveCondition(String providerId) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider> providerFactory = (ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider>) sessionFactory.getProviderFactory(ResourcePolicyConditionProvider.class, providerId);
|
||||
WorkflowConditionProviderFactory<WorkflowConditionProvider> providerFactory = (WorkflowConditionProviderFactory<WorkflowConditionProvider>) sessionFactory.getProviderFactory(WorkflowConditionProvider.class, providerId);
|
||||
|
||||
if (providerFactory == null) {
|
||||
throw new IllegalStateException("Could not find condition provider: " + providerId);
|
||||
@@ -118,7 +118,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
|
||||
}
|
||||
}
|
||||
|
||||
ResourcePolicyConditionProvider condition = providerFactory.create(session, config);
|
||||
WorkflowConditionProvider condition = providerFactory.create(session, config);
|
||||
|
||||
if (condition == null) {
|
||||
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -8,13 +8,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class EventBasedResourcePolicyProviderFactory implements ResourcePolicyProviderFactory {
|
||||
public class EventBasedWorkflowProviderFactory implements WorkflowProviderFactory {
|
||||
|
||||
public static final String ID = "event-based-resource-policy";
|
||||
public static final String ID = "event-based-workflow";
|
||||
|
||||
@Override
|
||||
public ResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new EventBasedResourcePolicyProvider(session, model);
|
||||
public WorkflowProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new EventBasedWorkflowProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaDelete;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class JpaWorkflowStateProvider implements WorkflowStateProvider {
|
||||
|
||||
private final EntityManager em;
|
||||
private static final Logger LOGGER = Logger.getLogger(JpaWorkflowStateProvider.class);
|
||||
private final KeycloakSession session;
|
||||
|
||||
public JpaWorkflowStateProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledStep getScheduledStep(String workflowId, String resourceId) {
|
||||
WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflowId);
|
||||
WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk);
|
||||
if (entity != null) {
|
||||
return new ScheduledStep(entity.getWorkflowId(), entity.getScheduledStepId(), entity.getResourceId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleStep(Workflow workflow, WorkflowStep step, String resourceId) {
|
||||
WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflow.getId());
|
||||
WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk);
|
||||
if (entity == null) {
|
||||
entity = new WorkflowStateEntity();
|
||||
entity.setResourceId(resourceId);
|
||||
entity.setWorkflowId(workflow.getId());
|
||||
entity.setWorkflowProviderId(workflow.getProviderId());
|
||||
entity.setScheduledStepId(step.getId());
|
||||
entity.setScheduledStepTimestamp(Time.currentTimeMillis() + step.getAfter());
|
||||
em.persist(entity);
|
||||
} else {
|
||||
entity.setScheduledStepId(step.getId());
|
||||
entity.setScheduledStepTimestamp(Time.currentTimeMillis() + step.getAfter());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledStep> getDueScheduledSteps(Workflow workflow) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
|
||||
|
||||
Predicate byWorkflow = cb.equal(stateRoot.get("workflowId"), workflow.getId());
|
||||
Predicate isExpired = cb.lessThan(stateRoot.get("scheduledStepTimestamp"), Time.currentTimeMillis());
|
||||
|
||||
query.where(cb.and(byWorkflow, isExpired));
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledStep> getScheduledStepsByWorkflow(String workflowId) {
|
||||
if (StringUtil.isBlank(workflowId)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
|
||||
|
||||
Predicate byWorkflow = cb.equal(stateRoot.get("workflowId"), workflowId);
|
||||
query.where(byWorkflow);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledStep> getScheduledStepsByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
|
||||
|
||||
Predicate byResource = cb.equal(stateRoot.get("resourceId"), resourceId);
|
||||
query.where(byResource);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<WorkflowStateEntity> delete = cb.createCriteriaDelete(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> root = delete.from(WorkflowStateEntity.class);
|
||||
delete.where(cb.equal(root.get("resourceId"), resourceId));
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
LOGGER.tracev("Deleted {0} orphaned state records for resource {1}", deletedCount, resourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String workflowId, String resourceId) {
|
||||
WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflowId);
|
||||
WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk);
|
||||
if (entity != null) {
|
||||
em.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String workflowId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<WorkflowStateEntity> delete = cb.createCriteriaDelete(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> root = delete.from(WorkflowStateEntity.class);
|
||||
delete.where(cb.equal(root.get("workflowId"), workflowId));
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<WorkflowStateEntity> delete = cb.createCriteriaDelete(WorkflowStateEntity.class);
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -23,7 +23,7 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel.RealmRemovedEvent;
|
||||
import org.keycloak.models.UserModel.UserRemovedEvent;
|
||||
|
||||
public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStateProviderFactory {
|
||||
public class JpaWorkflowStateProviderFactory implements WorkflowStateProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "jpa";
|
||||
|
||||
@@ -48,8 +48,8 @@ public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStat
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePolicyStateProvider create(KeycloakSession session) {
|
||||
return new JpaResourcePolicyStateProvider(session);
|
||||
public WorkflowStateProvider create(KeycloakSession session) {
|
||||
return new JpaWorkflowStateProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,13 +58,13 @@ public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStat
|
||||
|
||||
private void onRealmRemovedEvent(RealmRemovedEvent event) {
|
||||
KeycloakSession session = event.getKeycloakSession();
|
||||
ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
|
||||
provider.removeAll();
|
||||
}
|
||||
|
||||
private void onUserRemovedEvent(UserRemovedEvent event) {
|
||||
KeycloakSession session = event.getKeycloakSession();
|
||||
ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
|
||||
provider.removeByResource(event.getUser().getId());
|
||||
}
|
||||
}
|
||||
@@ -15,23 +15,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.util.List;
|
||||
import static org.keycloak.models.workflow.ResourceOperationType.CREATE;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceOperationType.CREATE;
|
||||
public class UserCreationTimeWorkflowProvider extends AbstractUserWorkflowProvider {
|
||||
|
||||
public class UserCreationTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
|
||||
|
||||
public UserCreationTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
public UserCreationTimeWorkflowProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isActivationEvent(ResourcePolicyEvent event) {
|
||||
protected boolean isActivationEvent(WorkflowEvent event) {
|
||||
return super.isActivationEvent(event) || CREATE.equals(event.getOperation());
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class UserSessionRefreshTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserSessionRefreshTimeResourcePolicyProvider> {
|
||||
public class UserCreationTimeWorkflowProviderFactory implements WorkflowProviderFactory<UserCreationTimeWorkflowProvider> {
|
||||
|
||||
public static final String ID = "user-refresh-time-resource-policy";
|
||||
public static final String ID = "user-creation-time-workflow";
|
||||
|
||||
@Override
|
||||
public UserSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserSessionRefreshTimeResourcePolicyProvider(session, model);
|
||||
public UserCreationTimeWorkflowProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserCreationTimeWorkflowProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -15,29 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceOperationType.CREATE;
|
||||
import static org.keycloak.models.policy.ResourceOperationType.LOGIN;
|
||||
import static org.keycloak.models.workflow.ResourceOperationType.CREATE;
|
||||
import static org.keycloak.models.workflow.ResourceOperationType.LOGIN;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class UserSessionRefreshTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
|
||||
public class UserSessionRefreshTimeWorkflowProvider extends AbstractUserWorkflowProvider {
|
||||
|
||||
public UserSessionRefreshTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
public UserSessionRefreshTimeWorkflowProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isActivationEvent(ResourcePolicyEvent event) {
|
||||
protected boolean isActivationEvent(WorkflowEvent event) {
|
||||
return super.isActivationEvent(event) || List.of(CREATE, LOGIN).contains(event.getOperation());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isResetEvent(ResourcePolicyEvent event) {
|
||||
protected boolean isResetEvent(WorkflowEvent event) {
|
||||
return LOGIN.equals(event.getOperation());
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class UserCreationTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserCreationTimeResourcePolicyProvider> {
|
||||
public class UserSessionRefreshTimeWorkflowProviderFactory implements WorkflowProviderFactory<UserSessionRefreshTimeWorkflowProvider> {
|
||||
|
||||
public static final String ID = "user-creation-time-resource-policy";
|
||||
public static final String ID = "user-refresh-time-workflow";
|
||||
|
||||
@Override
|
||||
public UserCreationTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserCreationTimeResourcePolicyProvider(session, model);
|
||||
public UserSessionRefreshTimeWorkflowProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserSessionRefreshTimeWorkflowProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
@@ -26,32 +26,32 @@ import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents the state of a resource within a time-based policy flow.
|
||||
* Represents the state of a resource within a time-based workflow.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "RESOURCE_POLICY_STATE")
|
||||
@IdClass(ResourcePolicyStateEntity.PrimaryKey.class)
|
||||
public class ResourcePolicyStateEntity {
|
||||
@Table(name = "WORKFLOW_STATE")
|
||||
@IdClass(WorkflowStateEntity.PrimaryKey.class)
|
||||
public class WorkflowStateEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "RESOURCE_ID")
|
||||
private String resourceId;
|
||||
|
||||
@Id
|
||||
@Column(name = "POLICY_ID")
|
||||
private String policyId;
|
||||
@Column(name = "WORKFLOW_ID")
|
||||
private String workflowId;
|
||||
|
||||
@Column(name = "RESOURCE_TYPE")
|
||||
private String resourceType; // do we want/need to store this?
|
||||
|
||||
@Column(name = "POLICY_PROVIDER_ID")
|
||||
private String policyProviderId;
|
||||
@Column(name = "WORKFLOW_PROVIDER_ID")
|
||||
private String workflowProviderId;
|
||||
|
||||
@Column(name = "SCHEDULED_ACTION_ID")
|
||||
private String scheduledActionId;
|
||||
@Column(name = "SCHEDULED_STEP_ID")
|
||||
private String scheduledStepId;
|
||||
|
||||
@Column(name = "SCHEDULED_ACTION_TIMESTAMP")
|
||||
private long scheduledActionTimestamp;
|
||||
@Column(name = "SCHEDULED_STEP_TIMESTAMP")
|
||||
private long scheduledStepTimestamp;
|
||||
|
||||
public String getResourceId() {
|
||||
return resourceId;
|
||||
@@ -61,20 +61,20 @@ public class ResourcePolicyStateEntity {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getPolicyId() {
|
||||
return policyId;
|
||||
public String getWorkflowId() {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
public void setPolicyId(String policyId) {
|
||||
this.policyId = policyId;
|
||||
public void setWorkflowId(String workflowId) {
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
public String getPolicyProviderId() {
|
||||
return policyProviderId;
|
||||
public String getWorkflowProviderId() {
|
||||
return workflowProviderId;
|
||||
}
|
||||
|
||||
public void setPolicyProviderId(String policyProviderId) {
|
||||
this.policyProviderId = policyProviderId;
|
||||
public void setWorkflowProviderId(String workflowProviderId) {
|
||||
this.workflowProviderId = workflowProviderId;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
@@ -85,33 +85,33 @@ public class ResourcePolicyStateEntity {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public String getScheduledActionId() {
|
||||
return scheduledActionId;
|
||||
public String getScheduledStepId() {
|
||||
return scheduledStepId;
|
||||
}
|
||||
|
||||
public void setScheduledActionId(String scheduledActionId) {
|
||||
this.scheduledActionId = scheduledActionId;
|
||||
public void setScheduledStepId(String scheduledStepId) {
|
||||
this.scheduledStepId = scheduledStepId;
|
||||
}
|
||||
|
||||
public long getScheduledActionTimestamp() {
|
||||
return scheduledActionTimestamp;
|
||||
public long getScheduledStepTimestamp() {
|
||||
return scheduledStepTimestamp;
|
||||
}
|
||||
|
||||
public void setScheduledActionTimestamp(long scheduledActionTimestamp) {
|
||||
this.scheduledActionTimestamp = scheduledActionTimestamp;
|
||||
public void setScheduledStepTimestamp(long scheduledStepTimestamp) {
|
||||
this.scheduledStepTimestamp = scheduledStepTimestamp;
|
||||
}
|
||||
|
||||
public static class PrimaryKey implements Serializable {
|
||||
|
||||
private String resourceId;
|
||||
private String policyId;
|
||||
private String workflowId;
|
||||
|
||||
public PrimaryKey() {
|
||||
}
|
||||
|
||||
public PrimaryKey(String resourceId, String policyId) {
|
||||
public PrimaryKey(String resourceId, String workflowId) {
|
||||
this.resourceId = resourceId;
|
||||
this.policyId = policyId;
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
public String getResourceId() {
|
||||
@@ -122,12 +122,12 @@ public class ResourcePolicyStateEntity {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getPolicyId() {
|
||||
return policyId;
|
||||
public String getWorkflowId() {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
public void setPolicyId(String policyId) {
|
||||
this.policyId = policyId;
|
||||
public void setWorkflowId(String workflowId) {
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,12 +135,12 @@ public class ResourcePolicyStateEntity {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PrimaryKey that = (PrimaryKey) o;
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId);
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(workflowId, that.workflowId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(resourceId, policyId);
|
||||
return Objects.hash(resourceId, workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,13 +148,13 @@ public class ResourcePolicyStateEntity {
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ResourcePolicyStateEntity that = (ResourcePolicyStateEntity) o;
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId);
|
||||
WorkflowStateEntity that = (WorkflowStateEntity) o;
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(workflowId, that.workflowId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(resourceId, policyId);
|
||||
return Objects.hash(resourceId, workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class GroupMembershipPolicyConditionFactory implements ResourcePolicyConditionProviderFactory<GroupMembershipPolicyConditionProvider> {
|
||||
public class GroupMembershipWorkflowConditionFactory implements WorkflowConditionProviderFactory<GroupMembershipWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "group-membership-condition";
|
||||
public static final String EXPECTED_GROUPS = "groups";
|
||||
|
||||
@Override
|
||||
public GroupMembershipPolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new GroupMembershipPolicyConditionProvider(session, config.get(EXPECTED_GROUPS));
|
||||
public GroupMembershipWorkflowConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new GroupMembershipWorkflowConditionProvider(session, config.get(EXPECTED_GROUPS));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -6,23 +6,23 @@ import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyEvent;
|
||||
import org.keycloak.models.policy.ResourcePolicyInvalidStateException;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowEvent;
|
||||
import org.keycloak.models.workflow.WorkflowInvalidStateException;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
|
||||
public class GroupMembershipPolicyConditionProvider implements ResourcePolicyConditionProvider {
|
||||
public class GroupMembershipWorkflowConditionProvider implements WorkflowConditionProvider {
|
||||
|
||||
private final List<String> expectedGroups;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public GroupMembershipPolicyConditionProvider(KeycloakSession session, List<String> expectedGroups) {
|
||||
public GroupMembershipWorkflowConditionProvider(KeycloakSession session, List<String> expectedGroups) {
|
||||
this.session = session;
|
||||
this.expectedGroups = expectedGroups;;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ResourcePolicyEvent event) {
|
||||
public boolean evaluate(WorkflowEvent event) {
|
||||
if (!ResourceType.USERS.equals(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class GroupMembershipPolicyConditionProvider implements ResourcePolicyCon
|
||||
public void validate() {
|
||||
expectedGroups.forEach(id -> {
|
||||
if (session.groups().getGroupById(session.getContext().getRealm(), id) == null) {
|
||||
throw new ResourcePolicyInvalidStateException(String.format("Group with id %s does not exist.", id));
|
||||
throw new WorkflowInvalidStateException(String.format("Group with id %s does not exist.", id));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class IdentityProviderPolicyConditionFactory implements ResourcePolicyConditionProviderFactory<IdentityProviderPolicyConditionProvider> {
|
||||
public class IdentityProviderWorkflowConditionFactory implements WorkflowConditionProviderFactory<IdentityProviderWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "identity-provider-condition";
|
||||
public static final String EXPECTED_ALIASES = "alias";
|
||||
|
||||
@Override
|
||||
public IdentityProviderPolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new IdentityProviderPolicyConditionProvider(session, config.get(EXPECTED_ALIASES));
|
||||
public IdentityProviderWorkflowConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new IdentityProviderWorkflowConditionProvider(session, config.get(EXPECTED_ALIASES));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
@@ -13,23 +13,23 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyEvent;
|
||||
import org.keycloak.models.policy.ResourcePolicyInvalidStateException;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowEvent;
|
||||
import org.keycloak.models.workflow.WorkflowInvalidStateException;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
|
||||
public class IdentityProviderPolicyConditionProvider implements ResourcePolicyConditionProvider {
|
||||
public class IdentityProviderWorkflowConditionProvider implements WorkflowConditionProvider {
|
||||
|
||||
private final List<String> expectedAliases;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public IdentityProviderPolicyConditionProvider(KeycloakSession session, List<String> expectedAliases) {
|
||||
public IdentityProviderWorkflowConditionProvider(KeycloakSession session, List<String> expectedAliases) {
|
||||
this.session = session;
|
||||
this.expectedAliases = expectedAliases;;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ResourcePolicyEvent event) {
|
||||
public boolean evaluate(WorkflowEvent event) {
|
||||
if (!ResourceType.USERS.equals(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class IdentityProviderPolicyConditionProvider implements ResourcePolicyCo
|
||||
public void validate() {
|
||||
expectedAliases.forEach(alias -> {
|
||||
if (session.identityProviders().getByAlias(alias) == null) {
|
||||
throw new ResourcePolicyInvalidStateException(String.format("Identity provider %s does not exist.", alias));
|
||||
throw new WorkflowInvalidStateException(String.format("Identity provider %s does not exist.", alias));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class RolePolicyConditionFactory implements ResourcePolicyConditionProviderFactory<RolePolicyConditionProvider> {
|
||||
public class RoleWorkflowConditionFactory implements WorkflowConditionProviderFactory<RoleWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "role-condition";
|
||||
public static final String EXPECTED_ROLES = "roles";
|
||||
|
||||
@Override
|
||||
public RolePolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new RolePolicyConditionProvider(session, config.get(EXPECTED_ROLES));
|
||||
public RoleWorkflowConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new RoleWorkflowConditionProvider(session, config.get(EXPECTED_ROLES));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -9,24 +9,24 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyEvent;
|
||||
import org.keycloak.models.policy.ResourcePolicyInvalidStateException;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowEvent;
|
||||
import org.keycloak.models.workflow.WorkflowInvalidStateException;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
public class RolePolicyConditionProvider implements ResourcePolicyConditionProvider {
|
||||
public class RoleWorkflowConditionProvider implements WorkflowConditionProvider {
|
||||
|
||||
private final List<String> expectedRoles;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public RolePolicyConditionProvider(KeycloakSession session, List<String> expectedRoles) {
|
||||
public RoleWorkflowConditionProvider(KeycloakSession session, List<String> expectedRoles) {
|
||||
this.session = session;
|
||||
this.expectedRoles = expectedRoles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ResourcePolicyEvent event) {
|
||||
public boolean evaluate(WorkflowEvent event) {
|
||||
if (!ResourceType.USERS.equals(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,10 +53,10 @@ public class RolePolicyConditionProvider implements ResourcePolicyConditionProvi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws ResourcePolicyInvalidStateException {
|
||||
public void validate() throws WorkflowInvalidStateException {
|
||||
expectedRoles.forEach(id -> {
|
||||
if (session.roles().getRoleById(session.getContext().getRealm(), id) == null) {
|
||||
throw new ResourcePolicyInvalidStateException(String.format("Role with id %s does not exist.", id));
|
||||
throw new WorkflowInvalidStateException(String.format("Role with id %s does not exist.", id));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProviderFactory;
|
||||
|
||||
public class UserAttributePolicyConditionFactory implements ResourcePolicyConditionProviderFactory<UserAttributePolicyConditionProvider> {
|
||||
public class UserAttributeWorkflowConditionFactory implements WorkflowConditionProviderFactory<UserAttributeWorkflowConditionProvider> {
|
||||
|
||||
public static final String ID = "user-attribute-condition";
|
||||
|
||||
@Override
|
||||
public UserAttributePolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new UserAttributePolicyConditionProvider(session, config);
|
||||
public UserAttributeWorkflowConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new UserAttributeWorkflowConditionProvider(session, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import static org.keycloak.common.util.CollectionUtil.collectionEquals;
|
||||
|
||||
@@ -9,22 +9,22 @@ import java.util.Map.Entry;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyEvent;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowEvent;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
|
||||
public class UserAttributePolicyConditionProvider implements ResourcePolicyConditionProvider {
|
||||
public class UserAttributeWorkflowConditionProvider implements WorkflowConditionProvider {
|
||||
|
||||
private final Map<String, List<String>> expectedAttributes;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public UserAttributePolicyConditionProvider(KeycloakSession session, Map<String, List<String>> expectedAttributes) {
|
||||
public UserAttributeWorkflowConditionProvider(KeycloakSession session, Map<String, List<String>> expectedAttributes) {
|
||||
this.session = session;
|
||||
this.expectedAttributes = expectedAttributes;;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ResourcePolicyEvent event) {
|
||||
public boolean evaluate(WorkflowEvent event) {
|
||||
if (!ResourceType.USERS.equals(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -28,35 +28,35 @@
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="40343-RLM-resource-policy-state-table" author="keycloak">
|
||||
<createTable tableName="RESOURCE_POLICY_STATE">
|
||||
<changeSet id="40343-workflow-state-table" author="keycloak">
|
||||
<createTable tableName="WORKFLOW_STATE">
|
||||
<column name="RESOURCE_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="POLICY_ID" type="VARCHAR(255)">
|
||||
<column name="WORKFLOW_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="POLICY_PROVIDER_ID" type="VARCHAR(255)" />
|
||||
<column name="WORKFLOW_PROVIDER_ID" type="VARCHAR(255)" />
|
||||
<column name="RESOURCE_TYPE" type="VARCHAR(255)" />
|
||||
<column name="SCHEDULED_ACTION_ID" type="VARCHAR(255)" />
|
||||
<column name="SCHEDULED_ACTION_TIMESTAMP" type="BIGINT" />
|
||||
<column name="SCHEDULED_STEP_ID" type="VARCHAR(255)" />
|
||||
<column name="SCHEDULED_STEP_TIMESTAMP" type="BIGINT" />
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey
|
||||
constraintName="PK_RESOURCE_POLICY_STATE"
|
||||
tableName="RESOURCE_POLICY_STATE"
|
||||
columnNames="RESOURCE_ID, POLICY_ID" />
|
||||
constraintName="PK_WORKFLOW_STEP_STATE"
|
||||
tableName="WORKFLOW_STATE"
|
||||
columnNames="RESOURCE_ID, WORKFLOW_ID" />
|
||||
|
||||
<createIndex indexName="IDX_RES_POLICY_STATE_ACTION"
|
||||
tableName="RESOURCE_POLICY_STATE">
|
||||
<column name="POLICY_ID" />
|
||||
<column name="SCHEDULED_ACTION_ID" />
|
||||
<createIndex indexName="IDX_WORKFLOW_STATE_STEP"
|
||||
tableName="WORKFLOW_STATE">
|
||||
<column name="WORKFLOW_ID" />
|
||||
<column name="SCHEDULED_STEP_ID" />
|
||||
</createIndex>
|
||||
|
||||
<createIndex indexName="IDX_RES_POLICY_STATE_PROVIDER"
|
||||
tableName="RESOURCE_POLICY_STATE">
|
||||
<createIndex indexName="IDX_WORKFLOW_STATE_PROVIDER"
|
||||
tableName="WORKFLOW_STATE">
|
||||
<column name="RESOURCE_ID" />
|
||||
<column name="POLICY_PROVIDER_ID" />
|
||||
<column name="WORKFLOW_PROVIDER_ID" />
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.UserAttributePolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.RolePolicyConditionFactory
|
||||
org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory
|
||||
org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory
|
||||
org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory
|
||||
org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory
|
||||
org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory
|
||||
org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory
|
||||
org.keycloak.models.workflow.EventBasedWorkflowProviderFactory
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.JpaResourcePolicyStateProviderFactory
|
||||
org.keycloak.models.workflow.JpaWorkflowStateProviderFactory
|
||||
@@ -92,8 +92,8 @@
|
||||
<!-- Server Configuration -->
|
||||
<class>org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity</class>
|
||||
|
||||
<!-- Resource Lifecycle Management -->
|
||||
<class>org.keycloak.models.policy.ResourcePolicyStateEntity</class>
|
||||
<!-- Workflows -->
|
||||
<class>org.keycloak.models.workflow.WorkflowStateEntity</class>
|
||||
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface serves as state check for policy actions.
|
||||
* Interface serves as state check for workflow actions.
|
||||
*/
|
||||
public interface ResourcePolicyStateProvider extends Provider {
|
||||
public interface WorkflowStateProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Deletes the state records associated with the given {@code resourceId}.
|
||||
@@ -34,40 +34,40 @@ public interface ResourcePolicyStateProvider extends Provider {
|
||||
void removeByResource(String resourceId);
|
||||
|
||||
/**
|
||||
* Removes the record identified by the specified {@code policyId} and {@code resourceId}.
|
||||
* @param policyId the id of the policy.
|
||||
* Removes the record identified by the specified {@code workflowId} and {@code resourceId}.
|
||||
* @param workflowId the id of the workflow.
|
||||
* @param resourceId the id of the resource.
|
||||
*/
|
||||
void remove(String policyId, String resourceId);
|
||||
void remove(String workflowId, String resourceId);
|
||||
|
||||
/**
|
||||
* Removes any record identified by the specified {@code policyId}.
|
||||
* @param policyId the id of the policy.
|
||||
* Removes any record identified by the specified {@code workflowId}.
|
||||
* @param workflowId the id of the workflow.
|
||||
*/
|
||||
void remove(String policyId);
|
||||
void remove(String workflowId);
|
||||
|
||||
/**
|
||||
* Deletes all state records associated with the current realm bound to the session.
|
||||
*/
|
||||
void removeAll();
|
||||
|
||||
void scheduleAction(ResourcePolicy policy, ResourceAction action, String resourceId);
|
||||
void scheduleStep(Workflow workflow, WorkflowStep step, String resourceId);
|
||||
|
||||
ScheduledAction getScheduledAction(String policyId, String resourceId);
|
||||
ScheduledStep getScheduledStep(String workflowId, String resourceId);
|
||||
|
||||
List<ScheduledAction> getScheduledActionsByResource(String resourceId);
|
||||
List<ScheduledStep> getScheduledStepsByResource(String resourceId);
|
||||
|
||||
List<ScheduledAction> getScheduledActionsByPolicy(String policy);
|
||||
List<ScheduledStep> getScheduledStepsByWorkflow(String workflowId);
|
||||
|
||||
default List<ScheduledAction> getScheduledActionsByPolicy(ResourcePolicy policy) {
|
||||
if (policy == null) {
|
||||
default List<ScheduledStep> getScheduledStepsByWorkflow(Workflow workflow) {
|
||||
if (workflow == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return getScheduledActionsByPolicy(policy.getId());
|
||||
return getScheduledStepsByWorkflow(workflow.getId());
|
||||
}
|
||||
|
||||
List<ScheduledAction> getDueScheduledActions(ResourcePolicy policy);
|
||||
List<ScheduledStep> getDueScheduledSteps(Workflow workflow);
|
||||
|
||||
record ScheduledAction (String policyId, String actionId, String resourceId) {};
|
||||
record ScheduledStep(String workflowId, String stepId, String resourceId) {}
|
||||
}
|
||||
@@ -15,18 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface ResourcePolicyStateProviderFactory extends ProviderFactory<ResourcePolicyStateProvider>, EnvironmentDependentProviderFactory {
|
||||
public interface WorkflowStateProviderFactory extends ProviderFactory<WorkflowStateProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,15 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourcePolicyStateSpi implements Spi {
|
||||
public class WorkflowStateSpi implements Spi {
|
||||
|
||||
public static final String NAME = "resource-policy-state";
|
||||
public static final String NAME = "workflow-state";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
@@ -37,11 +37,11 @@ public class ResourcePolicyStateSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourcePolicyStateProvider.class;
|
||||
return WorkflowStateProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourcePolicyStateProviderFactory.class;
|
||||
return WorkflowStateProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,4 @@ org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi
|
||||
org.keycloak.models.session.UserSessionPersisterSpi
|
||||
org.keycloak.models.session.RevokedTokenPersisterSpi
|
||||
org.keycloak.cluster.ClusterSpi
|
||||
org.keycloak.models.policy.ResourcePolicyStateSpi
|
||||
org.keycloak.models.workflow.WorkflowStateSpi
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import org.keycloak.models.ModelValidationException;
|
||||
|
||||
public class ResourcePolicyInvalidStateException extends ModelValidationException {
|
||||
|
||||
public ResourcePolicyInvalidStateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceOperationType.toOperationType;
|
||||
import static org.keycloak.models.workflow.ResourceOperationType.toOperationType;
|
||||
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventType;
|
||||
@@ -54,32 +54,32 @@ public enum ResourceType {
|
||||
this.resourceResolver = resourceResolver;
|
||||
}
|
||||
|
||||
public ResourcePolicyEvent toEvent(AdminEvent event) {
|
||||
public WorkflowEvent toEvent(AdminEvent event) {
|
||||
if (Objects.equals(this.supportedAdminResourceType, event.getResourceType())
|
||||
&& this.supportedAdminOperationTypes.contains(event.getOperationType())) {
|
||||
|
||||
ResourceOperationType resourceOperationType = toOperationType(event.getOperationType());
|
||||
if (resourceOperationType != null) {
|
||||
return new ResourcePolicyEvent(this, resourceOperationType, event.getResourceId(), event);
|
||||
return new WorkflowEvent(this, resourceOperationType, event.getResourceId(), event);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ResourcePolicyEvent toEvent(Event event) {
|
||||
public WorkflowEvent toEvent(Event event) {
|
||||
if (this.supportedEventTypes.contains(event.getType())) {
|
||||
ResourceOperationType resourceOperationType = toOperationType(event.getType());
|
||||
String resourceId = switch (this) {
|
||||
case USERS -> event.getUserId();
|
||||
};
|
||||
if (resourceOperationType != null && resourceId != null) {
|
||||
return new ResourcePolicyEvent(this, resourceOperationType, event.getUserId(), event);
|
||||
return new WorkflowEvent(this, resourceOperationType, event.getUserId(), event);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ResourcePolicyEvent toEvent(ProviderEvent event) {
|
||||
public WorkflowEvent toEvent(ProviderEvent event) {
|
||||
ResourceOperationType resourceOperationType = toOperationType(event.getClass());
|
||||
|
||||
if (resourceOperationType == null) {
|
||||
@@ -92,7 +92,7 @@ public enum ResourceType {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ResourcePolicyEvent(this, resourceOperationType, resourceId, event);
|
||||
return new WorkflowEvent(this, resourceOperationType, resourceId, event);
|
||||
}
|
||||
|
||||
public Object resolveResource(KeycloakSession session, String id) {
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,30 +23,30 @@ import java.util.Map;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class ResourcePolicy {
|
||||
public class Workflow {
|
||||
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private String providerId;
|
||||
private String id;
|
||||
private Long notBefore;
|
||||
|
||||
public ResourcePolicy() {
|
||||
public Workflow() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourcePolicy(String providerId) {
|
||||
public Workflow(String providerId) {
|
||||
this.providerId = providerId;
|
||||
this.id = null;
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
public ResourcePolicy(ComponentModel c) {
|
||||
public Workflow(ComponentModel c) {
|
||||
this.id = c.getId();
|
||||
this.providerId = c.getProviderId();
|
||||
this.config = c.getConfig();
|
||||
}
|
||||
|
||||
public ResourcePolicy(String providerId, Map<String, List<String>> config) {
|
||||
public Workflow(String providerId, Map<String, List<String>> config) {
|
||||
this.providerId = providerId;
|
||||
MultivaluedHashMap<String, String> c = new MultivaluedHashMap<>();
|
||||
config.forEach(c::addAll);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
@@ -6,13 +6,13 @@ import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface ResourcePolicyConditionProvider extends Provider {
|
||||
public interface WorkflowConditionProvider extends Provider {
|
||||
|
||||
boolean evaluate(ResourcePolicyEvent event);
|
||||
boolean evaluate(WorkflowEvent event);
|
||||
|
||||
default Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> userRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
void validate() throws ResourcePolicyInvalidStateException;
|
||||
void validate() throws WorkflowInvalidStateException;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -9,7 +9,7 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface ResourcePolicyConditionProviderFactory<P extends ResourcePolicyConditionProvider> extends ProviderFactory<P>, EnvironmentDependentProviderFactory {
|
||||
public interface WorkflowConditionProviderFactory<P extends WorkflowConditionProvider> extends ProviderFactory<P>, EnvironmentDependentProviderFactory {
|
||||
|
||||
P create(KeycloakSession session, Map<String, List<String>> config);
|
||||
|
||||
@@ -20,6 +20,6 @@ public interface ResourcePolicyConditionProviderFactory<P extends ResourcePolicy
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourcePolicyConditionSpi implements Spi {
|
||||
public class WorkflowConditionSpi implements Spi {
|
||||
|
||||
public static final String NAME = "rlm-policy-condition";
|
||||
public static final String NAME = "workflow-condition";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
@@ -20,11 +20,11 @@ public class ResourcePolicyConditionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourcePolicyConditionProvider.class;
|
||||
return WorkflowConditionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourcePolicyConditionProviderFactory.class;
|
||||
return WorkflowConditionProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
public class ResourcePolicyEvent {
|
||||
public class WorkflowEvent {
|
||||
|
||||
private final ResourceType type;
|
||||
private final ResourceOperationType operation;
|
||||
private final String resourceId;
|
||||
private final Object event;
|
||||
|
||||
public ResourcePolicyEvent(ResourceType type, ResourceOperationType operation, String resourceId, Object event) {
|
||||
public WorkflowEvent(ResourceType type, ResourceOperationType operation, String resourceId, Object event) {
|
||||
this.type = type;
|
||||
this.operation = operation;
|
||||
this.resourceId = resourceId;
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.models.ModelValidationException;
|
||||
|
||||
public class WorkflowInvalidStateException extends ModelValidationException {
|
||||
|
||||
public WorkflowInvalidStateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -15,19 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface ResourcePolicyProvider extends Provider {
|
||||
public interface WorkflowProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Finds all resources that are eligible for the first action of a policy.
|
||||
* Finds all resources that are eligible for the first action of a workflow.
|
||||
*
|
||||
* @return A list of eligible resource IDs.
|
||||
*/
|
||||
List<String> getEligibleResourcesForInitialAction();
|
||||
List<String> getEligibleResourcesForInitialStep();
|
||||
|
||||
/**
|
||||
* Checks if the provider supports resources of the specified type.
|
||||
@@ -38,45 +38,45 @@ public interface ResourcePolicyProvider extends Provider {
|
||||
boolean supports(ResourceType type);
|
||||
|
||||
/**
|
||||
* Indicates whether the policy supports being activated for a resource based on the event or not. If {@code true}, the
|
||||
* policy will be activated for the resource. For scheduled policies, this means the first action will be scheduled. For
|
||||
* immediate policies, this means all actions will be executed right away.
|
||||
* Indicates whether the workflow supports being activated for a resource based on the event or not. If {@code true}, the
|
||||
* workflow will be activated for the resource. For scheduled workflows, this means the first action will be scheduled. For
|
||||
* immediate workflows, this means all actions will be executed right away.
|
||||
*
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the policy will
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the workflow will
|
||||
* only be activated on expected operations being performed on the expected type.
|
||||
*
|
||||
* @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation
|
||||
* @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation
|
||||
* (CREATE, LOGIN, etc.), the resource type, and the resource id.
|
||||
* @return {@code true} if the policy can be activated based on the received event; {@code false} otherwise.
|
||||
* @return {@code true} if the workflow can be activated based on the received event; {@code false} otherwise.
|
||||
*/
|
||||
boolean activateOnEvent(ResourcePolicyEvent event);
|
||||
boolean activateOnEvent(WorkflowEvent event);
|
||||
|
||||
/**
|
||||
* Indicates whether the policy supports being reset (i.e. go back to the first action) based on the event received or not.
|
||||
* By default, this method returns false as most policies won't support this kind of flow, but specific policies such
|
||||
* Indicates whether the workflow supports being reset (i.e. go back to the first action) based on the event received or not.
|
||||
* By default, this method returns false as most workflows won't support this kind of flow, but specific workflows such
|
||||
* as one based on a resource's last updated time, or last used time, can signal that they expect the process to start
|
||||
* over once the timestamp they are based on is updated.
|
||||
*
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the policy will
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the workflow will
|
||||
* only be reset on expected operations being performed on the expected type.
|
||||
*
|
||||
* @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation
|
||||
* @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation
|
||||
* (CREATE, LOGIN, etc.), the resource type, and the resource id.
|
||||
* @return {@code true} if the policy supports resetting the flow based on the received event; {@code false} otherwise.
|
||||
* @return {@code true} if the workflow supports resetting the flow based on the received event; {@code false} otherwise.
|
||||
*/
|
||||
boolean resetOnEvent(ResourcePolicyEvent event);
|
||||
boolean resetOnEvent(WorkflowEvent event);
|
||||
|
||||
/**
|
||||
* Indicates whether the policy supports being deactivated for a resource based on the event or not. If {@code true}, the
|
||||
* policy will be deactivated for the resource, meaning any existing scheduled actions will be removed and no further
|
||||
* Indicates whether the workflow supports being deactivated for a resource based on the event or not. If {@code true}, the
|
||||
* workflow will be deactivated for the resource, meaning any existing scheduled actions will be removed and no further
|
||||
* actions will be executed.
|
||||
*
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the policy will
|
||||
* At the very least, implementations should validate the event's resource type and operation to ensure the workflow will
|
||||
* only be deactivated on expected operations being performed on the expected type.
|
||||
*
|
||||
* @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation
|
||||
* @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation
|
||||
* (CREATE, LOGIN, etc.), the resource type, and the resource id.
|
||||
* @return {@code true} if the policy can be deactivated based on the received event; {@code false} otherwise.
|
||||
* @return {@code true} if the workflow can be deactivated based on the received event; {@code false} otherwise.
|
||||
*/
|
||||
boolean deactivateOnEvent(ResourcePolicyEvent event);
|
||||
boolean deactivateOnEvent(WorkflowEvent event);
|
||||
}
|
||||
@@ -15,17 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public interface ResourcePolicyProviderFactory<P extends ResourcePolicyProvider> extends ComponentFactory<P, ResourcePolicyProvider>, EnvironmentDependentProviderFactory {
|
||||
public interface WorkflowProviderFactory<P extends WorkflowProvider> extends ComponentFactory<P, WorkflowProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS);
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourceActionSpi implements Spi {
|
||||
public class WorkflowSpi implements Spi {
|
||||
|
||||
public static String NAME = "rlm-action";
|
||||
public static final String NAME = "workflow";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
@@ -37,11 +37,11 @@ public class ResourceActionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourceActionProvider.class;
|
||||
return WorkflowProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourceActionProviderFactory.class;
|
||||
return WorkflowProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class ResourceAction implements Comparable<ResourceAction> {
|
||||
public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
|
||||
public static final String AFTER_KEY = "after";
|
||||
public static final String PRIORITY_KEY = "priority";
|
||||
@@ -30,23 +30,19 @@ public class ResourceAction implements Comparable<ResourceAction> {
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<ResourceAction> actions = List.of();
|
||||
private List<WorkflowStep> steps = List.of();
|
||||
|
||||
public ResourceAction() {
|
||||
public WorkflowStep() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourceAction(String providerId) {
|
||||
this.providerId = providerId;
|
||||
}
|
||||
|
||||
public ResourceAction(String providerId, MultivaluedHashMap<String, String> config, List<ResourceAction> actions) {
|
||||
public WorkflowStep(String providerId, MultivaluedHashMap<String, String> config, List<WorkflowStep> steps) {
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
this.actions = actions;
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public ResourceAction(ComponentModel model) {
|
||||
public WorkflowStep(ComponentModel model) {
|
||||
this.id = model.getId();
|
||||
this.providerId = model.getProviderId();
|
||||
this.config = model.getConfig();
|
||||
@@ -60,7 +56,7 @@ public class ResourceAction implements Comparable<ResourceAction> {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public ResourceAction setConfig(String key, String value) {
|
||||
public WorkflowStep setConfig(String key, String value) {
|
||||
if (config == null) {
|
||||
config = new MultivaluedHashMap<>();
|
||||
}
|
||||
@@ -79,7 +75,6 @@ public class ResourceAction implements Comparable<ResourceAction> {
|
||||
setConfig(PRIORITY_KEY, String.valueOf(priority));
|
||||
}
|
||||
|
||||
// todo, do not expose the priority to user?? in export etc. the order of actions should define the priority
|
||||
public int getPriority() {
|
||||
String value = getConfig().getFirst(PRIORITY_KEY);
|
||||
if (value == null) {
|
||||
@@ -100,19 +95,19 @@ public class ResourceAction implements Comparable<ResourceAction> {
|
||||
return Long.valueOf(getConfig().getFirstOrDefault(AFTER_KEY, "0"));
|
||||
}
|
||||
|
||||
public List<ResourceAction> getActions() {
|
||||
if (actions == null) {
|
||||
public List<WorkflowStep> getSteps() {
|
||||
if (steps == null) {
|
||||
return List.of();
|
||||
}
|
||||
return actions;
|
||||
return steps;
|
||||
}
|
||||
|
||||
public void setActions(List<ResourceAction> actions) {
|
||||
this.actions = actions;
|
||||
public void setSteps(List<WorkflowStep> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ResourceAction other) {
|
||||
public int compareTo(WorkflowStep other) {
|
||||
return Integer.compare(this.getPriority(), other.getPriority());
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface ResourceActionProvider extends Provider {
|
||||
public interface WorkflowStepProvider extends Provider {
|
||||
|
||||
void run(List<String> resourceIds);
|
||||
|
||||
boolean isRunnable();
|
||||
}
|
||||
@@ -15,19 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public interface ResourceActionProviderFactory<P extends ResourceActionProvider> extends ComponentFactory<P, ResourceActionProvider>, EnvironmentDependentProviderFactory {
|
||||
public interface WorkflowStepProviderFactory<P extends WorkflowStepProvider> extends ComponentFactory<P, WorkflowStepProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
ResourceType getType();
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS);
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourcePolicySpi implements Spi {
|
||||
public class WorkflowStepSpi implements Spi {
|
||||
|
||||
public static final String NAME = "rlm-policy";
|
||||
public static String NAME = "workflow-step";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
@@ -37,11 +37,11 @@ public class ResourcePolicySpi implements Spi {
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourcePolicyProvider.class;
|
||||
return WorkflowStepProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourcePolicyProviderFactory.class;
|
||||
return WorkflowStepProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ org.keycloak.cookie.CookieSpi
|
||||
org.keycloak.organization.OrganizationSpi
|
||||
org.keycloak.securityprofile.SecurityProfileSpi
|
||||
org.keycloak.logging.MappedDiagnosticContextSpi
|
||||
org.keycloak.models.policy.ResourceActionSpi
|
||||
org.keycloak.models.policy.ResourcePolicySpi
|
||||
org.keycloak.models.policy.ResourcePolicyConditionSpi
|
||||
org.keycloak.models.workflow.WorkflowStepSpi
|
||||
org.keycloak.models.workflow.WorkflowSpi
|
||||
org.keycloak.models.workflow.WorkflowConditionSpi
|
||||
org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorSpi
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
final class AdhocResourcePolicyEvent extends ResourcePolicyEvent {
|
||||
|
||||
AdhocResourcePolicyEvent(ResourceType type, String resourceId) {
|
||||
super(type, ResourceOperationType.AD_HOC, resourceId, null);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class AggregatedActionProvider implements ResourceActionProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel model;
|
||||
private final Logger log = Logger.getLogger(AggregatedActionProvider.class);
|
||||
|
||||
public AggregatedActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
List<ResourceActionProvider> actions = manager.getActionById(session, model.getId())
|
||||
.getActions().stream()
|
||||
.map(manager::getActionProvider)
|
||||
.toList();
|
||||
|
||||
for (String userId : userIds) {
|
||||
for (ResourceActionProvider action : actions) {
|
||||
try {
|
||||
action.run(List.of(userId));
|
||||
} catch (Exception e) {
|
||||
log.errorf(e, "Failed to execute action %s for user %s", model.getProviderId(), userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
public final class ResourcePolicyActionRunnerSuccessEvent implements ProviderEvent {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ResourcePolicyActionRunnerSuccessEvent(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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 jakarta.ws.rs.BadRequestException;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
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.RealmModel;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider.ScheduledAction;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
|
||||
public class ResourcePolicyManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(ResourcePolicyManager.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ResourcePolicyStateProvider policyStateProvider;
|
||||
|
||||
public static boolean isFeatureEnabled() {
|
||||
return Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
|
||||
public ResourcePolicyManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.policyStateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(String providerId) {
|
||||
return addPolicy(new ResourcePolicy(providerId));
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(String providerId, Map<String, List<String>> config) {
|
||||
return addPolicy(new ResourcePolicy(providerId, config));
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(ResourcePolicy policy) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel model = new ComponentModel();
|
||||
|
||||
model.setParentId(realm.getId());
|
||||
model.setProviderId(policy.getProviderId());
|
||||
model.setProviderType(ResourcePolicyProvider.class.getName());
|
||||
|
||||
MultivaluedHashMap<String, String> config = policy.getConfig();
|
||||
|
||||
if (config != null) {
|
||||
model.setConfig(config);
|
||||
}
|
||||
|
||||
return new ResourcePolicy(realm.addComponentModel(model));
|
||||
}
|
||||
|
||||
// This method takes an ordered list of actions. First action in the list has the highest priority, last action has the lowest priority
|
||||
public void createActions(ResourcePolicy policy, List<ResourceAction> actions) {
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
ResourceAction action = actions.get(i);
|
||||
|
||||
// assign priority based on index.
|
||||
action.setPriority(i + 1);
|
||||
|
||||
List<ResourceAction> subActions = Optional.ofNullable(action.getActions()).orElse(List.of());
|
||||
|
||||
// persist the new action component.
|
||||
action = addAction(policy.getId(), action);
|
||||
|
||||
for (int j = 0; j < subActions.size(); j++) {
|
||||
ResourceAction subAction = subActions.get(j);
|
||||
// assign priority based on index.
|
||||
subAction.setPriority(j + 1);
|
||||
addAction(action.getId(), subAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceAction addAction(String parentId, ResourceAction action) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel policyModel = realm.getComponent(parentId);
|
||||
ComponentModel actionModel = new ComponentModel();
|
||||
|
||||
actionModel.setId(action.getId());//need to keep stable UUIDs not to break a link in state table
|
||||
actionModel.setParentId(policyModel.getId());
|
||||
actionModel.setProviderId(action.getProviderId());
|
||||
actionModel.setProviderType(ResourceActionProvider.class.getName());
|
||||
actionModel.setConfig(action.getConfig());
|
||||
|
||||
return new ResourceAction(realm.addComponentModel(actionModel));
|
||||
}
|
||||
|
||||
public List<ResourcePolicy> getPolicies() {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName())
|
||||
.map(ResourcePolicy::new).toList();
|
||||
}
|
||||
|
||||
public List<ResourceAction> getActions(String policyId) {
|
||||
return getActionsStream(policyId).toList();
|
||||
}
|
||||
|
||||
public Stream<ResourceAction> getActionsStream(String parentId) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
return realm.getComponentsStream(parentId, ResourceActionProvider.class.getName())
|
||||
.map(this::toResourceAction).sorted();
|
||||
}
|
||||
|
||||
private ResourceAction toResourceAction(ComponentModel model) {
|
||||
ResourceAction action = new ResourceAction(model);
|
||||
|
||||
action.setActions(getActions(action.getId()));
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
public ResourceAction getActionById(KeycloakSession session, String id) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ComponentModel component = realm.getComponent(id);
|
||||
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toResourceAction(component);
|
||||
}
|
||||
|
||||
private ResourceAction getFirstAction(ResourcePolicy policy) {
|
||||
ResourceAction action = getActions(policy.getId()).get(0);
|
||||
Long notBefore = policy.getNotBefore();
|
||||
|
||||
if (notBefore != null) {
|
||||
action.setAfter(notBefore);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) {
|
||||
ComponentFactory<?, ?> factory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(ResourcePolicyProvider.class, policy.getProviderId());
|
||||
return (ResourcePolicyProvider) factory.create(session, getRealm().getComponent(policy.getId()));
|
||||
}
|
||||
|
||||
public ResourceActionProvider getActionProvider(ResourceAction action) {
|
||||
ComponentFactory<?, ?> actionFactory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(ResourceActionProvider.class, action.getProviderId());
|
||||
return (ResourceActionProvider) actionFactory.create(session, getRealm().getComponent(action.getId()));
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
|
||||
public void removePolicies() {
|
||||
RealmModel realm = getRealm();
|
||||
realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName()).forEach(policy -> {
|
||||
realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent);
|
||||
realm.removeComponent(policy);
|
||||
});
|
||||
}
|
||||
|
||||
public void scheduleAllEligibleResources(ResourcePolicy policy) {
|
||||
if (policy.isEnabled()) {
|
||||
ResourcePolicyProvider provider = getPolicyProvider(policy);
|
||||
provider.getEligibleResourcesForInitialAction()
|
||||
.forEach(resourceId -> processEvent(List.of(policy), new AdhocResourcePolicyEvent(ResourceType.USERS, resourceId)));
|
||||
}
|
||||
}
|
||||
|
||||
public void processEvent(ResourcePolicyEvent event) {
|
||||
processEvent(getPolicies(), event);
|
||||
}
|
||||
|
||||
public void processEvent(List<ResourcePolicy> policies, ResourcePolicyEvent event) {
|
||||
List<String> currentlyAssignedPolicies = policyStateProvider.getScheduledActionsByResource(event.getResourceId())
|
||||
.stream().map(ScheduledAction::policyId).toList();
|
||||
|
||||
// iterate through the policies, and for those not yet assigned to the user check if they can be assigned
|
||||
policies.stream()
|
||||
.filter(policy -> policy.isEnabled() && !getActions(policy.getId()).isEmpty())
|
||||
.forEach(policy -> {
|
||||
ResourcePolicyProvider provider = getPolicyProvider(policy);
|
||||
try {
|
||||
if (!currentlyAssignedPolicies.contains(policy.getId())) {
|
||||
// if policy is not active for the resource, check if the provider allows activating based on the event
|
||||
if (provider.activateOnEvent(event)) {
|
||||
if (policy.isScheduled()) {
|
||||
// policy is scheduled, so we schedule the first action
|
||||
log.debugf("Scheduling first action of policy %s for resource %s based on event %s",
|
||||
policy.getId(), event.getResourceId(), event.getOperation());
|
||||
policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
} else {
|
||||
// policy is not scheduled, so we run all actions immediately
|
||||
log.debugf("Running all actions of policy %s for resource %s based on event %s",
|
||||
policy.getId(), event.getResourceId(), event.getOperation());
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s -> {
|
||||
getActions(policy.getId()).forEach(action -> getActionProvider(action).run(List.of(event.getResourceId())));
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (provider.resetOnEvent(event)) {
|
||||
policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
} else if (provider.deactivateOnEvent(event)) {
|
||||
policyStateProvider.remove(policy.getId(), event.getResourceId());
|
||||
}
|
||||
}
|
||||
} catch (ResourcePolicyInvalidStateException e) {
|
||||
policy.getConfig().putSingle("enabled", "false");
|
||||
policy.getConfig().putSingle("validation_error", e.getMessage());
|
||||
updatePolicy(policy, policy.getConfig());
|
||||
log.debugf("Policy %s was disabled due to: %s", policy.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void runScheduledActions() {
|
||||
this.getPolicies().stream().filter(ResourcePolicy::isEnabled).forEach(policy -> {
|
||||
|
||||
for (ScheduledAction scheduled : policyStateProvider.getDueScheduledActions(policy)) {
|
||||
List<ResourceAction> actions = getActions(policy.getId());
|
||||
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
ResourceAction currentAction = actions.get(i);
|
||||
|
||||
if (currentAction.getId().equals(scheduled.actionId())) {
|
||||
getActionProvider(currentAction).run(List.of(scheduled.resourceId()));
|
||||
|
||||
if (actions.size() > i + 1) {
|
||||
// schedule the next action using the time offset difference between the actions.
|
||||
ResourceAction nextAction = actions.get(i + 1);
|
||||
policyStateProvider.scheduleAction(policy, nextAction, scheduled.resourceId());
|
||||
} else {
|
||||
// this was the last action, check if the policy is recurring - i.e. if we need to schedule the first action again
|
||||
if (policy.isRecurring()) {
|
||||
ResourceAction firstAction = getFirstAction(policy);
|
||||
policyStateProvider.scheduleAction(policy, firstAction, scheduled.resourceId());
|
||||
} else {
|
||||
// not recurring, remove the state record
|
||||
policyStateProvider.remove(policy.getId(), scheduled.resourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removePolicy(String id) {
|
||||
RealmModel realm = getRealm();
|
||||
realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName())
|
||||
.filter(policy -> policy.getId().equals(id))
|
||||
.forEach(policy -> {
|
||||
realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent);
|
||||
realm.removeComponent(policy);
|
||||
});
|
||||
policyStateProvider.remove(id);
|
||||
}
|
||||
|
||||
public ResourcePolicy getPolicy(String id) {
|
||||
return new ResourcePolicy(getPolicyComponent(id));
|
||||
}
|
||||
|
||||
public void updatePolicy(ResourcePolicy policy, MultivaluedHashMap<String, String> config) {
|
||||
ComponentModel component = getPolicyComponent(policy.getId());
|
||||
component.setConfig(config);
|
||||
getRealm().updateComponent(component);
|
||||
}
|
||||
|
||||
private ComponentModel getPolicyComponent(String id) {
|
||||
ComponentModel component = getRealm().getComponent(id);
|
||||
|
||||
if (component == null || !ResourcePolicyProvider.class.getName().equals(component.getProviderType())) {
|
||||
throw new BadRequestException("Not a valid resource policy: " + id);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
public ResourcePolicyRepresentation toRepresentation(ResourcePolicy policy) {
|
||||
ResourcePolicyRepresentation rep = new ResourcePolicyRepresentation(policy.getId(), policy.getProviderId(), policy.getConfig());
|
||||
|
||||
for (ResourceAction action : getActions(policy.getId())) {
|
||||
rep.addAction(toRepresentation(action));
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
private ResourcePolicyActionRepresentation toRepresentation(ResourceAction action) {
|
||||
List<ResourcePolicyActionRepresentation> actions = action.getActions().stream().map(this::toRepresentation).toList();
|
||||
return new ResourcePolicyActionRepresentation(action.getId(), action.getProviderId(), action.getConfig(), actions);
|
||||
}
|
||||
|
||||
public ResourcePolicy toModel(ResourcePolicyRepresentation rep) {
|
||||
MultivaluedHashMap<String, String> config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>());
|
||||
List<ResourcePolicyConditionRepresentation> conditions = ofNullable(rep.getConditions()).orElse(List.of());
|
||||
|
||||
for (ResourcePolicyConditionRepresentation condition : conditions) {
|
||||
String conditionProviderId = condition.getProviderId();
|
||||
config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId);
|
||||
|
||||
for (Entry<String, List<String>> configEntry : condition.getConfig().entrySet()) {
|
||||
config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
ResourcePolicy policy = addPolicy(rep.getProviderId(), config);
|
||||
List<ResourceAction> actions = new ArrayList<>();
|
||||
|
||||
for (ResourcePolicyActionRepresentation actionRep : rep.getActions()) {
|
||||
actions.add(toModel(actionRep));
|
||||
}
|
||||
|
||||
createActions(policy, actions);
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private ResourceAction toModel(ResourcePolicyActionRepresentation rep) {
|
||||
List<ResourceAction> subActions = new ArrayList<>();
|
||||
|
||||
for (ResourcePolicyActionRepresentation subAction : ofNullable(rep.getActions()).orElse(List.of())) {
|
||||
subActions.add(toModel(subAction));
|
||||
}
|
||||
|
||||
return new ResourceAction(rep.getProviderId(), rep.getConfig(), subActions);
|
||||
}
|
||||
|
||||
public void bind(ResourcePolicy policy, ResourceType type, String resourceId) {
|
||||
processEvent(List.of(policy), new AdhocResourcePolicyEvent(type, resourceId));
|
||||
}
|
||||
|
||||
public Object resolveResource(ResourceType type, String resourceId) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(type, "resourceId");
|
||||
return type.resolveResource(session, resourceId);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class UserActionBuilder {
|
||||
|
||||
private final ResourceAction action;
|
||||
|
||||
private UserActionBuilder(ResourceAction action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public static UserActionBuilder builder(String providerId) {
|
||||
ResourceAction action = new ResourceAction(providerId);
|
||||
return new UserActionBuilder(action);
|
||||
}
|
||||
|
||||
public ResourceAction build() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public UserActionBuilder after(Duration duration) {
|
||||
action.setAfter(duration.toMillis());
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserActionBuilder withConfig(String key, String value) {
|
||||
action.setConfig(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
@@ -9,19 +6,19 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceAction.AFTER_KEY;
|
||||
import java.util.List;
|
||||
|
||||
public class AddRequiredActionProvider implements ResourceActionProvider {
|
||||
public class AddRequiredActionStepProvider implements WorkflowStepProvider {
|
||||
|
||||
public static String REQUIRED_ACTION_KEY = "action";
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(AddRequiredActionProvider.class);
|
||||
private final ComponentModel stepModel;
|
||||
private final Logger log = Logger.getLogger(AddRequiredActionStepProvider.class);
|
||||
|
||||
public AddRequiredActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
public AddRequiredActionStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
this.stepModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,21 +30,16 @@ public class AddRequiredActionProvider implements ResourceActionProvider {
|
||||
|
||||
if (user != null) {
|
||||
try {
|
||||
UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(actionModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
log.debugv("Adding required action {0} to user {1})", action, user.getId());
|
||||
user.addRequiredAction(action);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", actionModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get(AFTER_KEY) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,13 +10,13 @@ import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
public class AddRequiredActionProviderFactory implements ResourceActionProviderFactory<AddRequiredActionProvider>, ConfiguredProvider {
|
||||
public class AddRequiredActionStepProviderFactory implements WorkflowStepProviderFactory<AddRequiredActionStepProvider>, ConfiguredProvider {
|
||||
|
||||
public static final String ID = "set-user-required-action";
|
||||
|
||||
@Override
|
||||
public AddRequiredActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new AddRequiredActionProvider(session, model);
|
||||
public AddRequiredActionStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new AddRequiredActionStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
final class AdhocWorkflowEvent extends WorkflowEvent {
|
||||
|
||||
AdhocWorkflowEvent(ResourceType type, String resourceId) {
|
||||
super(type, ResourceOperationType.AD_HOC, resourceId, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class AggregatedStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel model;
|
||||
private final Logger log = Logger.getLogger(AggregatedStepProvider.class);
|
||||
|
||||
public AggregatedStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
List<WorkflowStepProvider> steps = manager.getStepById(session, model.getId())
|
||||
.getSteps().stream()
|
||||
.map(manager::getStepProvider)
|
||||
.toList();
|
||||
|
||||
for (String userId : userIds) {
|
||||
for (WorkflowStepProvider step : steps) {
|
||||
try {
|
||||
step.run(List.of(userId));
|
||||
} catch (Exception e) {
|
||||
log.errorf(e, "Failed to execute step %s for user %s", model.getProviderId(), userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -8,13 +8,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class AggregatedActionProviderFactory implements ResourceActionProviderFactory<AggregatedActionProvider> {
|
||||
public class AggregatedStepProviderFactory implements WorkflowStepProviderFactory<AggregatedStepProvider> {
|
||||
|
||||
public static final String ID = "aggregated-action-provider";
|
||||
public static final String ID = "aggregated-step-provider";
|
||||
|
||||
@Override
|
||||
public AggregatedActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new AggregatedActionProvider(session, model);
|
||||
public AggregatedStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new AggregatedStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -15,11 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceAction.AFTER_KEY;
|
||||
|
||||
import java.util.List;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
@@ -27,15 +23,15 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class DeleteUserActionProvider implements ResourceActionProvider {
|
||||
import java.util.List;
|
||||
|
||||
public class DeleteUserStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(DeleteUserActionProvider.class);
|
||||
private final Logger log = Logger.getLogger(DeleteUserStepProvider.class);
|
||||
|
||||
public DeleteUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
public DeleteUserStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,9 +53,4 @@ public class DeleteUserActionProvider implements ResourceActionProvider {
|
||||
session.users().removeUser(realm, user);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get(AFTER_KEY) != null;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DeleteUserActionProviderFactory implements ResourceActionProviderFactory<DeleteUserActionProvider> {
|
||||
public class DeleteUserStepProviderFactory implements WorkflowStepProviderFactory<DeleteUserStepProvider> {
|
||||
|
||||
public static final String ID = "delete-user-action-provider";
|
||||
public static final String ID = "delete-user-step-provider";
|
||||
|
||||
@Override
|
||||
public DeleteUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DeleteUserActionProvider(session, model);
|
||||
public DeleteUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DeleteUserStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -15,26 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceAction.AFTER_KEY;
|
||||
|
||||
import java.util.List;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class DisableUserActionProvider implements ResourceActionProvider {
|
||||
import java.util.List;
|
||||
|
||||
public class DisableUserStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(DisableUserActionProvider.class);
|
||||
private final Logger log = Logger.getLogger(DisableUserStepProvider.class);
|
||||
|
||||
public DisableUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
public DisableUserStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,9 +51,4 @@ public class DisableUserActionProvider implements ResourceActionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get(AFTER_KEY) != null;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DisableUserActionProviderFactory implements ResourceActionProviderFactory<DisableUserActionProvider> {
|
||||
public class DisableUserStepProviderFactory implements WorkflowStepProviderFactory<DisableUserStepProvider> {
|
||||
|
||||
public static final String ID = "disable-user-action-provider";
|
||||
public static final String ID = "disable-user-step-provider";
|
||||
|
||||
@Override
|
||||
public DisableUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DisableUserActionProvider(session, model);
|
||||
public DisableUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DisableUserStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -15,17 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceAction.AFTER_KEY;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailTemplateProvider;
|
||||
@@ -33,7 +25,14 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
import java.time.Duration;
|
||||
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";
|
||||
private static final String ACCOUNT_DELETE_NOTIFICATION_SUBJECT = "accountDeleteNotificationSubject";
|
||||
@@ -41,12 +40,12 @@ public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
private static final String ACCOUNT_DELETE_NOTIFICATION_BODY = "accountDeleteNotificationBody";
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(NotifyUserActionProvider.class);
|
||||
private final ComponentModel stepModel;
|
||||
private final Logger log = Logger.getLogger(NotifyUserStepProvider.class);
|
||||
|
||||
public NotifyUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
public NotifyUserStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
this.stepModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -79,70 +78,70 @@ public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
}
|
||||
|
||||
private String getSubjectKey() {
|
||||
String nextActionType = getNextActionType();
|
||||
String customSubjectKey = actionModel.getConfig().getFirst("custom_subject_key");
|
||||
String nextStepType = getNextStepType();
|
||||
String customSubjectKey = stepModel.getConfig().getFirst("custom_subject_key");
|
||||
|
||||
if (customSubjectKey != null && !customSubjectKey.trim().isEmpty()) {
|
||||
return customSubjectKey;
|
||||
}
|
||||
|
||||
// Return default subject key based on next action type
|
||||
return getDefaultSubjectKey(nextActionType);
|
||||
// Return default subject key based on next step type
|
||||
return getDefaultSubjectKey(nextStepType);
|
||||
}
|
||||
|
||||
private String getBodyTemplate() {
|
||||
return "resource-policy-notification.ftl";
|
||||
return "workflow-notification.ftl";
|
||||
}
|
||||
|
||||
private Map<String, Object> getBodyAttributes() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
String nextActionType = getNextActionType();
|
||||
String nextStepType = getNextStepType();
|
||||
|
||||
// Custom message override or default based on action type
|
||||
String customMessage = actionModel.getConfig().getFirst("custom_message");
|
||||
// Custom message override or default based on step type
|
||||
String customMessage = stepModel.getConfig().getFirst("custom_message");
|
||||
if (customMessage != null && !customMessage.trim().isEmpty()) {
|
||||
attributes.put("messageKey", "customMessage");
|
||||
attributes.put("customMessage", customMessage);
|
||||
} else {
|
||||
attributes.put("messageKey", getDefaultMessageKey(nextActionType));
|
||||
attributes.put("messageKey", getDefaultMessageKey(nextStepType));
|
||||
}
|
||||
|
||||
// Calculate days remaining until next action
|
||||
int daysRemaining = calculateDaysUntilNextAction();
|
||||
// Calculate days remaining until next step
|
||||
int daysRemaining = calculateDaysUntilNextStep();
|
||||
|
||||
// Message parameters for internationalization
|
||||
attributes.put("daysRemaining", daysRemaining);
|
||||
attributes.put("reason", actionModel.getConfig().getFirstOrDefault("reason", "inactivity"));
|
||||
attributes.put("reason", stepModel.getConfig().getFirstOrDefault("reason", "inactivity"));
|
||||
attributes.put("realmName", realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName());
|
||||
attributes.put("nextActionType", nextActionType);
|
||||
attributes.put("nextStepType", nextStepType);
|
||||
attributes.put("subjectKey", getSubjectKey());
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private String getNextActionType() {
|
||||
Map<ComponentModel, Long> nextActionMap = getNextNonNotificationAction();
|
||||
return nextActionMap.isEmpty() ? "unknown-action" : nextActionMap.keySet().iterator().next().getProviderId();
|
||||
private String getNextStepType() {
|
||||
Map<ComponentModel, Long> nextStepMap = getNextNonNotificationStep();
|
||||
return nextStepMap.isEmpty() ? "unknown-step" : nextStepMap.keySet().iterator().next().getProviderId();
|
||||
}
|
||||
|
||||
private int calculateDaysUntilNextAction() {
|
||||
Map<ComponentModel, Long> nextActionMap = getNextNonNotificationAction();
|
||||
if (nextActionMap.isEmpty()) {
|
||||
private int calculateDaysUntilNextStep() {
|
||||
Map<ComponentModel, Long> nextStepMap = getNextNonNotificationStep();
|
||||
if (nextStepMap.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
Long timeToNextAction = nextActionMap.values().iterator().next();
|
||||
return Math.toIntExact(Duration.ofMillis(timeToNextAction).toDays());
|
||||
Long timeToNextStep = nextStepMap.values().iterator().next();
|
||||
return Math.toIntExact(Duration.ofMillis(timeToNextStep).toDays());
|
||||
}
|
||||
|
||||
private Map<ComponentModel, Long> getNextNonNotificationAction() {
|
||||
long timeToNextNonNotificationAction = 0L;
|
||||
private Map<ComponentModel, Long> getNextNonNotificationStep() {
|
||||
long timeToNextNonNotificationStep = 0L;
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ComponentModel policyModel = realm.getComponent(actionModel.getParentId());
|
||||
ComponentModel workflowModel = realm.getComponent(stepModel.getParentId());
|
||||
|
||||
List<ComponentModel> actions = realm.getComponentsStream(policyModel.getId(), ResourceActionProvider.class.getName())
|
||||
List<ComponentModel> steps = realm.getComponentsStream(workflowModel.getId(), WorkflowStepProvider.class.getName())
|
||||
.sorted((a, b) -> {
|
||||
int priorityA = Integer.parseInt(a.get("priority", "0"));
|
||||
int priorityB = Integer.parseInt(b.get("priority", "0"));
|
||||
@@ -150,17 +149,17 @@ public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Find current action and return next non-notification action
|
||||
// Find current step and return next non-notification step
|
||||
boolean foundCurrent = false;
|
||||
for (ComponentModel action : actions) {
|
||||
for (ComponentModel step : steps) {
|
||||
if (foundCurrent) {
|
||||
timeToNextNonNotificationAction += action.get(AFTER_KEY, 0L);
|
||||
if (!action.getProviderId().equals("notify-user-action-provider")) {
|
||||
timeToNextNonNotificationStep += step.get(AFTER_KEY, 0L);
|
||||
if (!step.getProviderId().equals("notify-user-step-provider")) {
|
||||
// we found the next non-notification action, accumulate its time and break
|
||||
return Map.of(action, timeToNextNonNotificationAction);
|
||||
return Map.of(step, timeToNextNonNotificationStep);
|
||||
}
|
||||
}
|
||||
if (action.getId().equals(actionModel.getId())) {
|
||||
if (step.getId().equals(stepModel.getId())) {
|
||||
foundCurrent = true;
|
||||
}
|
||||
}
|
||||
@@ -168,24 +167,19 @@ public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private String getDefaultSubjectKey(String actionType) {
|
||||
return switch (actionType) {
|
||||
case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT;
|
||||
case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT;
|
||||
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;
|
||||
default -> "accountNotificationSubject";
|
||||
};
|
||||
}
|
||||
|
||||
private String getDefaultMessageKey(String actionType) {
|
||||
return switch (actionType) {
|
||||
case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_BODY;
|
||||
case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_BODY;
|
||||
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;
|
||||
default -> "accountNotificationBody";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get(AFTER_KEY) != null;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -26,13 +26,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class NotifyUserActionProviderFactory implements ResourceActionProviderFactory<NotifyUserActionProvider> {
|
||||
public class NotifyUserStepProviderFactory implements WorkflowStepProviderFactory<NotifyUserStepProvider> {
|
||||
|
||||
public static final String ID = "notify-user-action-provider";
|
||||
public static final String ID = "notify-user-step-provider";
|
||||
|
||||
@Override
|
||||
public NotifyUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new NotifyUserActionProvider(session, model);
|
||||
public NotifyUserStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new NotifyUserStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +69,7 @@ public class NotifyUserActionProviderFactory implements ResourceActionProviderFa
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Arrays.asList(
|
||||
new ProviderConfigProperty("reason", "Reason",
|
||||
"Reason for the action (inactivity, policy violation, compliance requirement)",
|
||||
"Reason for the action (inactivity, workflow violation, compliance requirement)",
|
||||
ProviderConfigProperty.STRING_TYPE, ""),
|
||||
|
||||
new ProviderConfigProperty("custom_subject_key", "Custom Subject Message Key",
|
||||
@@ -15,13 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceAction.PRIORITY_KEY;
|
||||
import static org.keycloak.models.policy.ResourceAction.AFTER_KEY;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
@@ -29,15 +23,21 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class SetUserAttributeActionProvider implements ResourceActionProvider {
|
||||
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;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(SetUserAttributeActionProvider.class);
|
||||
private final ComponentModel stepModel;
|
||||
private final Logger log = Logger.getLogger(SetUserAttributeStepProvider.class);
|
||||
|
||||
public SetUserAttributeActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
public SetUserAttributeStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
this.stepModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,7 +52,7 @@ public class SetUserAttributeActionProvider implements ResourceActionProvider {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null) {
|
||||
for (Entry<String, List<String>> entry : actionModel.getConfig().entrySet()) {
|
||||
for (Entry<String, List<String>> entry : stepModel.getConfig().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
||||
if (!key.startsWith(AFTER_KEY) && !key.startsWith(PRIORITY_KEY)) {
|
||||
@@ -63,9 +63,4 @@ public class SetUserAttributeActionProvider implements ResourceActionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class SetUserAttributeActionProviderFactory implements ResourceActionProviderFactory<SetUserAttributeActionProvider> {
|
||||
public class SetUserAttributeStepProviderFactory implements WorkflowStepProviderFactory<SetUserAttributeStepProvider> {
|
||||
|
||||
public static final String ID = "set-user-attr-action-provider";
|
||||
public static final String ID = "set-user-attr-step-provider";
|
||||
|
||||
@Override
|
||||
public SetUserAttributeActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new SetUserAttributeActionProvider(session, model);
|
||||
public SetUserAttributeStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new SetUserAttributeStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -61,7 +61,7 @@ public class SetUserAttributeActionProviderFactory implements ResourceActionProv
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Sets an attribute on the user. Configure attributes to set as 'user.attribute.<attribute-name>' in the action's configuration.";
|
||||
return "Sets an attribute on the user. Configure attributes to set as 'user.attribute.<attribute-name>' in the step's configuration.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -8,7 +8,7 @@
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventListenerProvider;
|
||||
@@ -18,24 +18,24 @@ import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
|
||||
public class ResourcePolicyEventListener implements EventListenerProvider, ProviderEventListener {
|
||||
public class WorkflowEventListener implements EventListenerProvider, ProviderEventListener {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ResourcePolicyEventListener(KeycloakSession session) {
|
||||
public WorkflowEventListener(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(Event event) {
|
||||
ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event);
|
||||
trySchedule(policyEvent);
|
||||
WorkflowEvent workflowEvent = ResourceType.USERS.toEvent(event);
|
||||
trySchedule(workflowEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(AdminEvent event, boolean includeRepresentation) {
|
||||
ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event);
|
||||
trySchedule(policyEvent);
|
||||
WorkflowEvent workflowEvent = ResourceType.USERS.toEvent(event);
|
||||
trySchedule(workflowEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,9 +49,9 @@ public class ResourcePolicyEventListener implements EventListenerProvider, Provi
|
||||
trySchedule(ResourceType.USERS.toEvent(event));
|
||||
}
|
||||
|
||||
private void trySchedule(ResourcePolicyEvent event) {
|
||||
private void trySchedule(WorkflowEvent event) {
|
||||
if (event != null) {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
manager.processEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
@@ -9,21 +9,21 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
|
||||
/**
|
||||
* A {@link ScheduledTask} that runs all the scheduled actions for resources on a per-realm basis.
|
||||
* A {@link ScheduledTask} that runs all the scheduled steps for resources on a per-realm basis.
|
||||
*/
|
||||
final class ResourceActionRunnerScheduledTask implements ScheduledTask {
|
||||
final class WorkflowRunnerScheduledTask implements ScheduledTask {
|
||||
|
||||
private final Logger logger = Logger.getLogger(ResourceActionRunnerScheduledTask.class);
|
||||
private final Logger logger = Logger.getLogger(WorkflowRunnerScheduledTask.class);
|
||||
|
||||
private final KeycloakSessionFactory sessionFactory;
|
||||
|
||||
ResourceActionRunnerScheduledTask(KeycloakSessionFactory sessionFactory) {
|
||||
WorkflowRunnerScheduledTask(KeycloakSessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
// TODO: Depending on how many realms and the actions in use, this task can consume a lot of gears (e.g.: cpu, memory, and network)
|
||||
// TODO: Depending on how many realms and the steps in use, this task can consume a lot of gears (e.g.: cpu, memory, and network)
|
||||
// we need a smarter mechanism that process realms in batches with some window interval
|
||||
session.realms().getRealmsStream().map(RealmModel::getId).forEach(this::runScheduledTasksOnRealm);
|
||||
}
|
||||
@@ -35,17 +35,17 @@ final class ResourceActionRunnerScheduledTask implements ScheduledTask {
|
||||
RealmModel realm = session.realms().getRealm(id);
|
||||
|
||||
context.setRealm(realm);
|
||||
new ResourcePolicyManager(session).runScheduledActions();
|
||||
new WorkflowsManager(session).runScheduledSteps();
|
||||
|
||||
sessionFactory.publish(new ResourcePolicyActionRunnerSuccessEvent(session));
|
||||
sessionFactory.publish(new WorkflowStepRunnerSuccessEvent(session));
|
||||
} catch (Exception e) {
|
||||
logger.errorf(e, "Failed to run resource policy actions on realm with id '%s'", id);
|
||||
logger.errorf(e, "Failed to run workflow steps on realm with id '%s'", id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTaskName() {
|
||||
return "resource-policy-action-runner";
|
||||
return "workflow-runner-task";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
public record WorkflowStepRunnerSuccessEvent(KeycloakSession session) implements ProviderEvent {
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@@ -30,15 +30,15 @@ import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
public class ResourcePolicyEventListenerFactory implements EventListenerProviderFactory, EnvironmentDependentProviderFactory {
|
||||
public class WorkflowsEventListenerFactory implements EventListenerProviderFactory, EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String ID = "resource-policy-event-listener";
|
||||
private static final long DEFAULT_ACTION_RUNNER_TASK_INTERVAL = Duration.ofHours(12).toMillis();
|
||||
private long actionRunnerTaskInterval;
|
||||
public static final String ID = "workflow-event-listener";
|
||||
private static final long DEFAULT_STEP_RUNNER_TASK_INTERVAL = Duration.ofHours(12).toMillis();
|
||||
private long stepRunnerTaskInterval;
|
||||
|
||||
@Override
|
||||
public EventListenerProvider create(KeycloakSession session) {
|
||||
return new ResourcePolicyEventListener(session);
|
||||
return new WorkflowEventListener(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,7 +48,7 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {
|
||||
actionRunnerTaskInterval = config.getLong("actionRunnerTaskInterval", DEFAULT_ACTION_RUNNER_TASK_INTERVAL);
|
||||
stepRunnerTaskInterval = config.getLong("stepRunnerTaskInterval", DEFAULT_STEP_RUNNER_TASK_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,11 +60,11 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider
|
||||
onEvent(event, session);
|
||||
}
|
||||
});
|
||||
scheduleActionRunnerTask(factory);
|
||||
scheduleStepRunnerTask(factory);
|
||||
}
|
||||
|
||||
private void onEvent(ProviderEvent event, KeycloakSession session) {
|
||||
ResourcePolicyEventListener provider = (ResourcePolicyEventListener) session.getProvider(EventListenerProvider.class, getId());
|
||||
WorkflowEventListener provider = (WorkflowEventListener) session.getProvider(EventListenerProvider.class, getId());
|
||||
provider.onEvent(event);
|
||||
}
|
||||
|
||||
@@ -79,13 +79,13 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS);
|
||||
}
|
||||
|
||||
private void scheduleActionRunnerTask(KeycloakSessionFactory factory) {
|
||||
private void scheduleStepRunnerTask(KeycloakSessionFactory factory) {
|
||||
try (KeycloakSession session = factory.create()) {
|
||||
TimerProvider timer = session.getProvider(TimerProvider.class);
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(factory, new ResourceActionRunnerScheduledTask(factory), actionRunnerTaskInterval), actionRunnerTaskInterval);
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(factory, new WorkflowRunnerScheduledTask(factory), stepRunnerTaskInterval), stepRunnerTaskInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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 jakarta.ws.rs.BadRequestException;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
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.RealmModel;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
public class WorkflowsManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(WorkflowsManager.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final WorkflowStateProvider workflowStateProvider;
|
||||
|
||||
public static boolean isFeatureEnabled() {
|
||||
return Profile.isFeatureEnabled(Feature.WORKFLOWS);
|
||||
}
|
||||
|
||||
public WorkflowsManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.workflowStateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
}
|
||||
|
||||
public Workflow addWorkflow(String providerId, Map<String, List<String>> config) {
|
||||
return addWorkflow(new Workflow(providerId, config));
|
||||
}
|
||||
|
||||
public Workflow addWorkflow(Workflow workflow) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel model = new ComponentModel();
|
||||
|
||||
model.setParentId(realm.getId());
|
||||
model.setProviderId(workflow.getProviderId());
|
||||
model.setProviderType(WorkflowProvider.class.getName());
|
||||
|
||||
MultivaluedHashMap<String, String> config = workflow.getConfig();
|
||||
|
||||
if (config != null) {
|
||||
model.setConfig(config);
|
||||
}
|
||||
|
||||
return new Workflow(realm.addComponentModel(model));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep step = steps.get(i);
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WorkflowStep addStep(String parentId, WorkflowStep step) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel workflowModel = realm.getComponent(parentId);
|
||||
ComponentModel stepModel = new ComponentModel();
|
||||
|
||||
stepModel.setId(step.getId());//need to keep stable UUIDs not to break a link in state table
|
||||
stepModel.setParentId(workflowModel.getId());
|
||||
stepModel.setProviderId(step.getProviderId());
|
||||
stepModel.setProviderType(WorkflowStepProvider.class.getName());
|
||||
stepModel.setConfig(step.getConfig());
|
||||
|
||||
return new WorkflowStep(realm.addComponentModel(stepModel));
|
||||
}
|
||||
|
||||
public List<Workflow> getWorkflows() {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName())
|
||||
.map(Workflow::new).toList();
|
||||
}
|
||||
|
||||
public List<WorkflowStep> getSteps(String workflowId) {
|
||||
return getStepsStream(workflowId).toList();
|
||||
}
|
||||
|
||||
public Stream<WorkflowStep> getStepsStream(String parentId) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
return realm.getComponentsStream(parentId, WorkflowStepProvider.class.getName())
|
||||
.map(this::toStep).sorted();
|
||||
}
|
||||
|
||||
private WorkflowStep toStep(ComponentModel model) {
|
||||
WorkflowStep step = new WorkflowStep(model);
|
||||
|
||||
step.setSteps(getSteps(step.getId()));
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
public WorkflowStep getStepById(KeycloakSession session, String id) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ComponentModel component = realm.getComponent(id);
|
||||
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toStep(component);
|
||||
}
|
||||
|
||||
private WorkflowStep getFirstStep(Workflow workflow) {
|
||||
WorkflowStep step = getSteps(workflow.getId()).get(0);
|
||||
Long notBefore = workflow.getNotBefore();
|
||||
|
||||
if (notBefore != null) {
|
||||
step.setAfter(notBefore);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private WorkflowProvider getWorkflowProvider(Workflow workflow) {
|
||||
ComponentFactory<?, ?> factory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(WorkflowProvider.class, workflow.getProviderId());
|
||||
return (WorkflowProvider) factory.create(session, getRealm().getComponent(workflow.getId()));
|
||||
}
|
||||
|
||||
public WorkflowStepProvider getStepProvider(WorkflowStep step) {
|
||||
ComponentFactory<?, ?> stepFactory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(WorkflowStepProvider.class, step.getProviderId());
|
||||
return (WorkflowStepProvider) stepFactory.create(session, getRealm().getComponent(step.getId()));
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
|
||||
public void removeWorkflows() {
|
||||
RealmModel realm = getRealm();
|
||||
realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName()).forEach(workflow -> {
|
||||
realm.getComponentsStream(workflow.getId(), WorkflowStepProvider.class.getName()).forEach(realm::removeComponent);
|
||||
realm.removeComponent(workflow);
|
||||
});
|
||||
}
|
||||
|
||||
public void scheduleAllEligibleResources(Workflow workflow) {
|
||||
if (workflow.isEnabled()) {
|
||||
WorkflowProvider provider = getWorkflowProvider(workflow);
|
||||
provider.getEligibleResourcesForInitialStep()
|
||||
.forEach(resourceId -> processEvent(List.of(workflow), new AdhocWorkflowEvent(ResourceType.USERS, resourceId)));
|
||||
}
|
||||
}
|
||||
|
||||
public void processEvent(WorkflowEvent event) {
|
||||
processEvent(getWorkflows(), event);
|
||||
}
|
||||
|
||||
public void processEvent(List<Workflow> workflows, WorkflowEvent event) {
|
||||
List<String> currentlyAssignedWorkflows = workflowStateProvider.getScheduledStepsByResource(event.getResourceId())
|
||||
.stream().map(ScheduledStep::workflowId).toList();
|
||||
|
||||
// iterate through the workflows, and for those not yet assigned to the user check if they can be assigned
|
||||
workflows.stream()
|
||||
.filter(workflow -> workflow.isEnabled() && !getSteps(workflow.getId()).isEmpty())
|
||||
.forEach(workflow -> {
|
||||
WorkflowProvider provider = getWorkflowProvider(workflow);
|
||||
try {
|
||||
if (!currentlyAssignedWorkflows.contains(workflow.getId())) {
|
||||
// if workflow is not active for the resource, check if the provider allows activating based on the event
|
||||
if (provider.activateOnEvent(event)) {
|
||||
if (workflow.isScheduled()) {
|
||||
// workflow is scheduled, so we schedule the first step
|
||||
log.debugf("Scheduling first step of workflow %s for resource %s based on event %s",
|
||||
workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
workflowStateProvider.scheduleStep(workflow, getFirstStep(workflow), event.getResourceId());
|
||||
} else {
|
||||
// workflow is not scheduled, so we run all steps immediately
|
||||
log.debugf("Running all steps of workflow %s for resource %s based on event %s",
|
||||
workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s ->
|
||||
getSteps(workflow.getId()).forEach(step -> getStepProvider(step).run(List.of(event.getResourceId())))
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (provider.resetOnEvent(event)) {
|
||||
workflowStateProvider.scheduleStep(workflow, getFirstStep(workflow), event.getResourceId());
|
||||
} else if (provider.deactivateOnEvent(event)) {
|
||||
workflowStateProvider.remove(workflow.getId(), event.getResourceId());
|
||||
}
|
||||
}
|
||||
} catch (WorkflowInvalidStateException e) {
|
||||
workflow.getConfig().putSingle("enabled", "false");
|
||||
workflow.getConfig().putSingle("validation_error", e.getMessage());
|
||||
updateWorkflow(workflow, workflow.getConfig());
|
||||
log.debugf("Workflow %s was disabled due to: %s", workflow.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void runScheduledSteps() {
|
||||
this.getWorkflows().stream().filter(Workflow::isEnabled).forEach(workflow -> {
|
||||
|
||||
for (ScheduledStep scheduled : workflowStateProvider.getDueScheduledSteps(workflow)) {
|
||||
List<WorkflowStep> steps = getSteps(workflow.getId());
|
||||
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep currentStep = steps.get(i);
|
||||
|
||||
if (currentStep.getId().equals(scheduled.stepId())) {
|
||||
getStepProvider(currentStep).run(List.of(scheduled.resourceId()));
|
||||
|
||||
if (steps.size() > i + 1) {
|
||||
// schedule the next step using the time offset difference between the steps.
|
||||
WorkflowStep nextStep = steps.get(i + 1);
|
||||
workflowStateProvider.scheduleStep(workflow, nextStep, scheduled.resourceId());
|
||||
} else {
|
||||
// this was the last step, check if the workflow is recurring - i.e. if we need to schedule the first step again
|
||||
if (workflow.isRecurring()) {
|
||||
WorkflowStep firstStep = getFirstStep(workflow);
|
||||
workflowStateProvider.scheduleStep(workflow, firstStep, scheduled.resourceId());
|
||||
} else {
|
||||
// not recurring, remove the state record
|
||||
workflowStateProvider.remove(workflow.getId(), scheduled.resourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeWorkflow(String id) {
|
||||
RealmModel realm = getRealm();
|
||||
realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName())
|
||||
.filter(workflow -> workflow.getId().equals(id))
|
||||
.forEach(workflow -> {
|
||||
realm.getComponentsStream(workflow.getId(), WorkflowStepProvider.class.getName()).forEach(realm::removeComponent);
|
||||
realm.removeComponent(workflow);
|
||||
});
|
||||
workflowStateProvider.remove(id);
|
||||
}
|
||||
|
||||
public Workflow getWorkflow(String id) {
|
||||
return new Workflow(getWorkflowComponent(id));
|
||||
}
|
||||
|
||||
public void updateWorkflow(Workflow workflow, MultivaluedHashMap<String, String> config) {
|
||||
ComponentModel component = getWorkflowComponent(workflow.getId());
|
||||
component.setConfig(config);
|
||||
getRealm().updateComponent(component);
|
||||
}
|
||||
|
||||
private ComponentModel getWorkflowComponent(String id) {
|
||||
ComponentModel component = getRealm().getComponent(id);
|
||||
|
||||
if (component == null || !WorkflowProvider.class.getName().equals(component.getProviderType())) {
|
||||
throw new BadRequestException("Not a valid resource workflow: " + id);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
public WorkflowRepresentation toRepresentation(Workflow workflow) {
|
||||
WorkflowRepresentation rep = new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig());
|
||||
|
||||
for (WorkflowStep step : getSteps(workflow.getId())) {
|
||||
rep.addStep(toRepresentation(step));
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
private WorkflowStepRepresentation toRepresentation(WorkflowStep step) {
|
||||
List<WorkflowStepRepresentation> steps = step.getSteps().stream().map(this::toRepresentation).toList();
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps);
|
||||
}
|
||||
|
||||
public Workflow toModel(WorkflowRepresentation rep) {
|
||||
MultivaluedHashMap<String, String> config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>());
|
||||
List<WorkflowConditionRepresentation> conditions = ofNullable(rep.getConditions()).orElse(List.of());
|
||||
|
||||
for (WorkflowConditionRepresentation condition : conditions) {
|
||||
String conditionProviderId = condition.getProviderId();
|
||||
config.computeIfAbsent("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);
|
||||
List<WorkflowStep> steps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation stepRep : rep.getSteps()) {
|
||||
steps.add(toModel(stepRep));
|
||||
}
|
||||
|
||||
createSteps(workflow, steps);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private WorkflowStep toModel(WorkflowStepRepresentation rep) {
|
||||
List<WorkflowStep> subSteps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation subStep : ofNullable(rep.getSteps()).orElse(List.of())) {
|
||||
subSteps.add(toModel(subStep));
|
||||
}
|
||||
|
||||
return new WorkflowStep(rep.getProviderId(), rep.getConfig(), subSteps);
|
||||
}
|
||||
|
||||
public void bind(Workflow workflow, ResourceType type, String resourceId) {
|
||||
processEvent(List.of(workflow), new AdhocWorkflowEvent(type, resourceId));
|
||||
}
|
||||
|
||||
public Object resolveResource(ResourceType type, String resourceId) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(type, "resourceId");
|
||||
return type.resolveResource(session, resourceId);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package org.keycloak.realm.resources.policies.admin.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
|
||||
class RealmResourcePoliciesResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ResourcePolicyManager manager;
|
||||
|
||||
public RealmResourcePoliciesResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
manager = new ResourcePolicyManager(session);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response create(ResourcePolicyRepresentation rep) {
|
||||
ResourcePolicy policy = manager.toModel(rep);
|
||||
return Response.created(session.getContext().getUri().getRequestUriBuilder().path(policy.getId()).build()).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response createAll(List<ResourcePolicyRepresentation> reps) {
|
||||
for (ResourcePolicyRepresentation policy : reps) {
|
||||
manager.toModel(policy);
|
||||
}
|
||||
return Response.created(session.getContext().getUri().getRequestUri()).build();
|
||||
}
|
||||
|
||||
@Path("{id}")
|
||||
public RealmResourcePolicyResource get(@PathParam("id") String id) {
|
||||
ResourcePolicy policy = manager.getPolicy(id);
|
||||
|
||||
if (policy == null) {
|
||||
throw new NotFoundException("Resource policy with id " + id + " not found");
|
||||
}
|
||||
|
||||
return new RealmResourcePolicyResource(manager, policy);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<ResourcePolicyRepresentation> list() {
|
||||
return manager.getPolicies().stream().map(manager::toRepresentation).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.keycloak.realm.resources.policies.admin.resource;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.Path;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class RealmResourcesResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public RealmResourcesResource(KeycloakSession session) {
|
||||
if (!Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Path("policies")
|
||||
public RealmResourcePoliciesResource policies() {
|
||||
return new RealmResourcePoliciesResource(session);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.organization.admin.resource.OrganizationsResource;
|
||||
import org.keycloak.partialimport.PartialImportResult;
|
||||
import org.keycloak.partialimport.PartialImportResults;
|
||||
import org.keycloak.realm.resources.policies.admin.resource.RealmResourcesResource;
|
||||
import org.keycloak.workflow.admin.resource.WorkflowsResource;
|
||||
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
@@ -637,9 +637,9 @@ public class RealmAdminResource {
|
||||
return new OrganizationsResource(session, auth, adminEvent);
|
||||
}
|
||||
|
||||
@Path("resources")
|
||||
public RealmResourcesResource resources() {
|
||||
return new RealmResourcesResource(session);
|
||||
@Path("workflows")
|
||||
public WorkflowsResource workflows() {
|
||||
return new WorkflowsResource(session);
|
||||
}
|
||||
|
||||
@Path("{extension}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.realm.resources.policies.admin.resource;
|
||||
package org.keycloak.workflow.admin.resource;
|
||||
|
||||
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
|
||||
|
||||
@@ -12,35 +12,35 @@ import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
class RealmResourcePolicyResource {
|
||||
public class WorkflowResource {
|
||||
|
||||
private final ResourcePolicyManager manager;
|
||||
private final ResourcePolicy policy;
|
||||
private final WorkflowsManager manager;
|
||||
private final Workflow workflow;
|
||||
|
||||
public RealmResourcePolicyResource(ResourcePolicyManager manager, ResourcePolicy policy) {
|
||||
public WorkflowResource(WorkflowsManager manager, Workflow workflow) {
|
||||
this.manager = manager;
|
||||
this.policy = policy;
|
||||
this.workflow = workflow;
|
||||
}
|
||||
|
||||
@DELETE
|
||||
public void delete(String id) {
|
||||
manager.removePolicy(policy.getId());
|
||||
public void delete() {
|
||||
manager.removeWorkflow(workflow.getId());
|
||||
}
|
||||
|
||||
@PUT
|
||||
public void update(ResourcePolicyRepresentation rep) {
|
||||
manager.updatePolicy(policy, rep.getConfig());
|
||||
public void update(WorkflowRepresentation rep) {
|
||||
manager.updateWorkflow(workflow, rep.getConfig());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(APPLICATION_JSON)
|
||||
public ResourcePolicyRepresentation toRepresentation() {
|
||||
return manager.toRepresentation(policy);
|
||||
public WorkflowRepresentation toRepresentation() {
|
||||
return manager.toRepresentation(workflow);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -54,9 +54,9 @@ class RealmResourcePolicyResource {
|
||||
}
|
||||
|
||||
if (notBefore != null) {
|
||||
policy.setNotBefore(notBefore);
|
||||
workflow.setNotBefore(notBefore);
|
||||
}
|
||||
|
||||
manager.bind(policy, type, resourceId);
|
||||
manager.bind(workflow, type, resourceId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.keycloak.workflow.admin.resource;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WorkflowsResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final WorkflowsManager manager;
|
||||
|
||||
public WorkflowsResource(KeycloakSession session) {
|
||||
if (!Profile.isFeatureEnabled(Feature.WORKFLOWS)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
this.session = session;
|
||||
this.manager = new WorkflowsManager(session);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response create(WorkflowRepresentation rep) {
|
||||
Workflow workflow = manager.toModel(rep);
|
||||
return Response.created(session.getContext().getUri().getRequestUriBuilder().path(workflow.getId()).build()).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response createAll(List<WorkflowRepresentation> reps) {
|
||||
for (WorkflowRepresentation workflow : reps) {
|
||||
manager.toModel(workflow);
|
||||
}
|
||||
return Response.created(session.getContext().getUri().getRequestUri()).build();
|
||||
}
|
||||
|
||||
@Path("{id}")
|
||||
public WorkflowResource get(@PathParam("id") String id) {
|
||||
Workflow workflow = manager.getWorkflow(id);
|
||||
|
||||
if (workflow == null) {
|
||||
throw new NotFoundException("Workflow with id " + id + " not found");
|
||||
}
|
||||
|
||||
return new WorkflowResource(manager, workflow);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<WorkflowRepresentation> list() {
|
||||
return manager.getWorkflows().stream().map(manager::toRepresentation).toList();
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,4 @@
|
||||
|
||||
org.keycloak.events.email.EmailEventListenerProviderFactory
|
||||
org.keycloak.events.log.JBossLoggingEventListenerProviderFactory
|
||||
org.keycloak.models.policy.ResourcePolicyEventListenerFactory
|
||||
org.keycloak.models.workflow.WorkflowsEventListenerFactory
|
||||
@@ -15,10 +15,9 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.DisableUserActionProviderFactory
|
||||
org.keycloak.models.policy.NotifyUserActionProviderFactory
|
||||
org.keycloak.models.policy.DeleteUserActionProviderFactory
|
||||
org.keycloak.models.policy.SetUserAttributeActionProviderFactory
|
||||
org.keycloak.models.policy.AggregatedActionProviderFactory
|
||||
org.keycloak.models.policy.AddRequiredActionProviderFactory
|
||||
|
||||
org.keycloak.models.workflow.DisableUserStepProviderFactory
|
||||
org.keycloak.models.workflow.NotifyUserStepProviderFactory
|
||||
org.keycloak.models.workflow.DeleteUserStepProviderFactory
|
||||
org.keycloak.models.workflow.SetUserAttributeStepProviderFactory
|
||||
org.keycloak.models.workflow.AggregatedStepProviderFactory
|
||||
org.keycloak.models.workflow.AddRequiredActionStepProviderFactory
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.AddRequiredActionProviderFactory;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class AddRequiredActionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@Test
|
||||
public void testActionRun() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.immediate()
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(AddRequiredActionProviderFactory.ID)
|
||||
.withConfig("action", "UPDATE_PASSWORD")
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close();
|
||||
|
||||
List< UserRepresentation> users = managedRealm.admin().users().search("test");
|
||||
assertThat(users, hasSize(1));
|
||||
UserRepresentation userRepresentation = users.get(0);
|
||||
assertThat(userRepresentation.getRequiredActions(), hasSize(1));
|
||||
assertThat(userRepresentation.getRequiredActions().get(0), is(UserModel.RequiredAction.UPDATE_PASSWORD.name()));
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import org.keycloak.models.policy.ResourcePolicyEventListenerFactory;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
public class RLMScheduledTaskServerConfig extends RLMServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config)
|
||||
.option("spi-events-listener--" + ResourcePolicyEventListenerFactory.ID + "--action-runner-task-interval", "1000");
|
||||
}
|
||||
}
|
||||
@@ -1,871 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.hamcrest.Matchers;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.DeleteUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceAction;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider.ScheduledAction;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.tests.utils.MailUtils;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class ResourcePolicyManagementTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectUser(ref = "alice", config = DefaultUserConfig.class, lifecycle = LifeCycle.METHOD)
|
||||
private ManagedUser userAlice;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
List<ResourcePolicyRepresentation> actualPolicies = policies.list();
|
||||
assertThat(actualPolicies, Matchers.hasSize(1));
|
||||
|
||||
assertThat(actualPolicies.get(0).getProviderId(), is(UserCreationTimeResourcePolicyProviderFactory.ID));
|
||||
assertThat(actualPolicies.get(0).getActions(), Matchers.hasSize(2));
|
||||
assertThat(actualPolicies.get(0).getActions().get(0).getProviderId(), is(NotifyUserActionProviderFactory.ID));
|
||||
assertThat(actualPolicies.get(0).getActions().get(1).getProviderId(), is(DisableUserActionProviderFactory.ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWithNoConditions() {
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
expectedPolicies.get(0).setConditions(null);
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
|
||||
policies.create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.recurring()
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.recurring()
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the only action in the policy
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> actualPolicies = policies.list();
|
||||
assertThat(actualPolicies, Matchers.hasSize(2));
|
||||
|
||||
ResourcePolicyRepresentation policy = actualPolicies.stream().filter(p -> UserCreationTimeResourcePolicyProviderFactory.ID.equals(p.getProviderId())).findAny().orElse(null);
|
||||
String id = policy.getId();
|
||||
policies.policy(id).delete().close();
|
||||
actualPolicies = policies.list();
|
||||
assertThat(actualPolicies, Matchers.hasSize(1));
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
List<ResourcePolicy> registeredPolicies = manager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
List<ScheduledAction> actions = stateProvider.getScheduledActionsByPolicy(id);
|
||||
assertTrue(actions.isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() {
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.name("test-policy")
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
List<ResourcePolicyRepresentation> actualPolicies = policies.list();
|
||||
assertThat(actualPolicies, Matchers.hasSize(1));
|
||||
ResourcePolicyRepresentation policy = actualPolicies.get(0);
|
||||
assertThat(policy.getName(), is("test-policy"));
|
||||
|
||||
policy.setName("changed");
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close();
|
||||
actualPolicies = policies.list();
|
||||
policy = actualPolicies.get(0);
|
||||
assertThat(policy.getName(), is("changed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPolicyDoesNotFallThroughActionsInSingleRun() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the first action
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm,"testuser");
|
||||
|
||||
List<ResourcePolicy> registeredPolicies = manager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
|
||||
ResourcePolicy policy = registeredPolicies.get(0);
|
||||
assertEquals(2, manager.getActions(policy.getId()).size());
|
||||
ResourceAction notifyAction = manager.getActions(policy.getId()).get(0);
|
||||
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(notifyAction.getId(), scheduledAction.actionId());
|
||||
|
||||
try {
|
||||
// Simulate the user being 12 days old, making them eligible for both actions' time conditions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
|
||||
// Verify that the next action was scheduled for the user
|
||||
ResourceAction disableAction = manager.getActions(policy.getId()).get(1);
|
||||
scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(disableAction.getId(), scheduledAction.actionId(), "The second action should have been scheduled");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignPolicyToExistingResources() {
|
||||
// create some realm users
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build()).close();
|
||||
}
|
||||
|
||||
// create some users associated with a federated identity
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "idp-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
|
||||
idp.setAlias("someidp");
|
||||
idp.setProviderId(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID);
|
||||
idp.setEnabled(true);
|
||||
managedRealm.admin().identityProviders().create(idp).close();
|
||||
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.name())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(IdentityProviderPolicyConditionFactory.ID)
|
||||
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, "someidp")
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// now with the policy in place, let's create a couple more idp users - these will be attached to the policy on
|
||||
// creation.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "new-idp-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
// new realm users created after the policy - these should not be attached to the policy because they are not idp users.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager policyManager = new ResourcePolicyManager(session);
|
||||
List<ResourcePolicy> registeredPolicies = policyManager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
ResourcePolicy policy = registeredPolicies.get(0);
|
||||
|
||||
assertEquals(2, policyManager.getActions(policy.getId()).size());
|
||||
ResourceAction notifyAction = policyManager.getActions(policy.getId()).get(0);
|
||||
|
||||
// check no policies are yet attached to the previous users, only to the ones created after the policy was in place
|
||||
ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(3, scheduledActions.size());
|
||||
scheduledActions.forEach(scheduledAction -> {
|
||||
assertEquals(notifyAction.getId(), scheduledAction.actionId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
try {
|
||||
// let's run the schedule actions for the new users so they transition to the next one.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
policyManager.runScheduledActions();
|
||||
|
||||
// check the same users are now scheduled to run the second action.
|
||||
ResourceAction disableAction = policyManager.getActions(policy.getId()).get(1);
|
||||
scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(3, scheduledActions.size());
|
||||
scheduledActions.forEach(scheduledAction -> {
|
||||
assertEquals(disableAction.getId(), scheduledAction.actionId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
// assign the policy to the eligible users - i.e. only users from the same idp who are not yet assigned to the policy.
|
||||
policyManager.scheduleAllEligibleResources(policy);
|
||||
|
||||
// check policy was correctly assigned to the old users, not affecting users already associated with the policy.
|
||||
scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(13, scheduledActions.size());
|
||||
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledToNotify = scheduledActions.stream()
|
||||
.filter(action -> notifyAction.getId().equals(action.actionId())).toList();
|
||||
assertEquals(10, scheduledToNotify.size());
|
||||
scheduledToNotify.forEach(scheduledAction -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("idp-user-"));
|
||||
});
|
||||
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledToDisable = scheduledActions.stream()
|
||||
.filter(action -> disableAction.getId().equals(action.actionId())).toList();
|
||||
assertEquals(3, scheduledToDisable.size());
|
||||
scheduledToDisable.forEach(scheduledAction -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableResourcePolicy() {
|
||||
// create a test policy
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.name("test-policy")
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
List<ResourcePolicyRepresentation> actualPolicies = policies.list();
|
||||
assertThat(actualPolicies, Matchers.hasSize(1));
|
||||
ResourcePolicyRepresentation policy = actualPolicies.get(0);
|
||||
assertThat(policy.getName(), is("test-policy"));
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the first action
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Advance time so the user is eligible for the first action, then run the scheduled actions so they transition to the next one.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// disable the policy - scheduled actions should be paused and policy should not activate for new users
|
||||
policy.getConfig().putSingle("enabled", "false");
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close();
|
||||
|
||||
// create another user - should NOT bind the user to the policy as it is disabled
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("anotheruser").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
List<ResourcePolicy> registeredPolicies = manager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledActions = stateProvider.getScheduledActionsByPolicy(registeredPolicies.get(0));
|
||||
|
||||
// verify that there's only one scheduled action, for the first user
|
||||
assertEquals(1, scheduledActions.size());
|
||||
UserModel scheduledActionUser = session.users().getUserById(realm, scheduledActions.get(0).resourceId());
|
||||
assertNotNull(scheduledActionUser);
|
||||
assertTrue(scheduledActionUser.getUsername().startsWith("testuser"));
|
||||
|
||||
try {
|
||||
// Advance time so the first user would be eligible for the second action, then run the scheduled actions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that the action was NOT executed as the policy is disabled.
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run as the policy is disabled.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// re-enable the policy - scheduled actions should resume and new users should be bound to the policy
|
||||
policy.getConfig().putSingle("enabled", "true");
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close();
|
||||
|
||||
// create a third user - should bind the user to the policy as it is enabled again
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").email("thirduser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Advance time so the first user would be eligible for the second action, and third user would be eligible for the first action, then run the scheduled actions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that the action was executed as the policy was re-enabled.
|
||||
assertFalse(user.isEnabled(), "The second action (disable) should have run as the policy was re-enabled.");
|
||||
|
||||
// Verify that the third user was bound to the policy
|
||||
user = session.users().getUserByUsername(realm, "thirduser");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first action (notify) was executed by checking email was sent
|
||||
testUserMessage = findEmailByRecipient(mailServer, "thirduser@example.com");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecurringPolicy() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.recurring()
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the only action in the policy
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
ResourcePolicy policy = manager.getPolicies().get(0);
|
||||
ResourceAction action = manager.getActions(policy.getId()).get(0);
|
||||
|
||||
// Verify that the action was scheduled again for the user
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(action.getId(), scheduledAction.actionId(), "The action should have been scheduled again");
|
||||
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that there should be two emails sent
|
||||
assertEquals(2, findEmailsByRecipient(mailServer, "testuser@example.com").size());
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunImmediatePolicy() {
|
||||
// create a test policy with no time conditions - should run immediately when scheduled
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.immediate()
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(SetUserAttributeActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should be bound to the new policy and all actions should run right away
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build()).close();
|
||||
|
||||
// check the user has the attribute set and is disabled
|
||||
runOnServer.run(session -> {
|
||||
configureSessionContext(session);
|
||||
UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "testuser");
|
||||
assertEquals("message", user.getAttributes().get("message").get(0));
|
||||
assertFalse(user.isEnabled());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSendsEmailWithDefaultDisableMessage() {
|
||||
// Create policy: disable at 10 days, notify 3 days before (at day 7)
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(3))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("test@example.com").name("John", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 7 days old (eligible for notify action)
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
|
||||
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test@example.com");
|
||||
verifyEmailContent(testUserMessage, "test@example.com", "Disable", "John", "3", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSendsEmailWithDefaultDeleteMessage() {
|
||||
// Create policy: delete at 30 days, notify 15 days before (at day 15)
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser2").email("test2@example.com").name("Jane", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 15 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test2@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test2@example.com");
|
||||
verifyEmailContent(testUserMessage, "test2@example.com", "Deletion", "Jane", "15", "inactivity", "permanently deleted");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionWithCustomMessageOverride() {
|
||||
// Create policy: disable at 7 days, notify 2 days before (at day 5) with custom message
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("reason", "compliance requirement")
|
||||
.withConfig("custom_message", "Your account requires immediate attention due to new compliance policies.")
|
||||
.withConfig("custom_subject_key", "customComplianceSubject")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(2))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser3").email("test3@example.com").name("Bob", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 5 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test3@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test3@example.com");
|
||||
verifyEmailContent(testUserMessage, "test3@example.com", "", "Bob", "2", "immediate attention due to new compliance policies");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserActionSkipsUsersWithoutEmailButLogsWarning() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// But should still create state record for the policy flow
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser4");
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
var scheduledActions = stateProvider.getScheduledActionsByResource(user.getId());
|
||||
assertEquals(1, scheduledActions.size());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Should NOT send email to user without email address
|
||||
MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4");
|
||||
assertNull(testUserMessage, "No email should be sent to user without email address");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompleteUserLifecycleWithMultipleNotifications() {
|
||||
// Create policy: just disable at 30 days with one notification before
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser5").email("testuser5@example.com").name("TestUser5", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser5");
|
||||
|
||||
try {
|
||||
// Day 15: First notification - this should run the notify action and schedule the disable action
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// Check that user is still enabled after notification
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertTrue(user.isEnabled(), "User should still be enabled after notification");
|
||||
|
||||
// Day 30 + 15 minutes: Disable user - run 15 minutes after the scheduled time to ensure it's due
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(30).toSeconds()) + Math.toIntExact(Duration.ofMinutes(15).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
|
||||
// Verify user is disabled
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertNotNull(user, "User should still exist after disable");
|
||||
assertFalse(user.isEnabled(), "User should be disabled");
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify notification was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser5@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for testuser5@example.com");
|
||||
verifyEmailContent(testUserMessage, "testuser5@example.com", "Disable", "TestUser5", "15", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
public static List<MimeMessage> findEmailsByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static MimeMessage findEmailByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private MimeMessage findEmailByRecipientContaining(String recipientPart) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).contains(recipientPart);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
|
||||
public static void verifyEmailContent(MimeMessage message, String expectedRecipient, String subjectContains,
|
||||
String... contentContains) {
|
||||
try {
|
||||
assertEquals(expectedRecipient, MailUtils.getRecipient(message));
|
||||
assertTrue(message.getSubject().contains(subjectContains),
|
||||
"Subject should contain '" + subjectContains + "'");
|
||||
|
||||
MailUtils.EmailBody body = MailUtils.getBody(message);
|
||||
String textContent = body.getText();
|
||||
String htmlContent = body.getHtml();
|
||||
|
||||
for (String expectedContent : contentContains) {
|
||||
boolean foundInText = textContent.contains(expectedContent);
|
||||
boolean foundInHtml = htmlContent.contains(expectedContent);
|
||||
assertTrue(foundInText || foundInHtml,
|
||||
"Email content should contain: " + expectedContent +
|
||||
"\nText: " + textContent +
|
||||
"\nHTML: " + htmlContent);
|
||||
}
|
||||
} catch (MessagingException | IOException e) {
|
||||
Assertions.fail("Failed to read email message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultUserConfig implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
user.username("alice");
|
||||
user.password("alice");
|
||||
user.name("alice", "alice");
|
||||
user.email("master-admin@email.org");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.workflow.AddRequiredActionStepProvider;
|
||||
import org.keycloak.models.workflow.AddRequiredActionStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class AddRequiredActionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@Test
|
||||
public void testStepRun() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(AddRequiredActionStepProviderFactory.ID)
|
||||
.withConfig(AddRequiredActionStepProvider.REQUIRED_ACTION_KEY, "UPDATE_PASSWORD")
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close();
|
||||
|
||||
List< UserRepresentation> users = managedRealm.admin().users().search("test");
|
||||
assertThat(users, hasSize(1));
|
||||
UserRepresentation userRepresentation = users.get(0);
|
||||
assertThat(userRepresentation.getRequiredActions(), hasSize(1));
|
||||
assertThat(userRepresentation.getRequiredActions().get(0), is(UserModel.RequiredAction.UPDATE_PASSWORD.name()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
@@ -15,14 +15,14 @@ import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
@@ -31,8 +31,8 @@ import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class AdhocPolicyTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class AdhocWorkflowTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -44,53 +44,53 @@ public class AdhocPolicyTest {
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
ResourcePolicyRepresentation policy = policies.get(0);
|
||||
assertThat(policy.getActions(), hasSize(1));
|
||||
ResourcePolicyActionRepresentation aggregatedAction = policy.getActions().get(0);
|
||||
assertThat(aggregatedAction.getProviderId(), is(SetUserAttributeActionProviderFactory.ID));
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
assertThat(workflow.getSteps(), hasSize(1));
|
||||
WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0);
|
||||
assertThat(aggregatedStep.getProviderId(), is(SetUserAttributeStepProviderFactory.ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunAdHocScheduledPolicy() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
public void testRunAdHocScheduledWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("message", "message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
ResourcePolicyRepresentation policy = policies.get(0);
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
|
||||
try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id);
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id);
|
||||
}
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
@@ -100,65 +100,65 @@ public class AdhocPolicyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunAdHocNonScheduledPolicy() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
public void testRunAdHocNonScheduledWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
ResourcePolicyRepresentation policy = policies.get(0);
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
|
||||
try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id);
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id);
|
||||
}
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunAdHocTimedScheduledPolicy() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
public void testRunAdHocTimedScheduledWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
ResourcePolicyRepresentation policy = policies.get(0);
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
String id;
|
||||
|
||||
try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) {
|
||||
id = ApiUtil.getCreatedId(response);
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(5).toMillis());
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(5).toMillis());
|
||||
}
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
@@ -167,19 +167,19 @@ public class AdhocPolicyTest {
|
||||
}
|
||||
}));
|
||||
|
||||
managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(10).toMillis());
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(10).toMillis());
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
@@ -188,7 +188,7 @@ public class AdhocPolicyTest {
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
@@ -17,15 +17,15 @@ import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.AggregatedActionProviderFactory;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.workflow.AggregatedStepProviderFactory;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
@@ -33,8 +33,8 @@ import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class AggregatedActionTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class AggregatedStepTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -46,36 +46,36 @@ public class AggregatedActionTest {
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(DisableUserActionProviderFactory.ID)
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
ResourcePolicyRepresentation policy = policies.get(0);
|
||||
assertThat(policy.getActions(), hasSize(1));
|
||||
ResourcePolicyActionRepresentation aggregatedAction = policy.getActions().get(0);
|
||||
assertThat(aggregatedAction.getProviderId(), is(AggregatedActionProviderFactory.ID));
|
||||
List<ResourcePolicyActionRepresentation> actions = aggregatedAction.getActions();
|
||||
assertThat(actions, hasSize(2));
|
||||
assertAction(actions, SetUserAttributeActionProviderFactory.ID, a -> {
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
assertThat(workflow.getSteps(), hasSize(1));
|
||||
WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0);
|
||||
assertThat(aggregatedStep.getProviderId(), is(AggregatedStepProviderFactory.ID));
|
||||
List<WorkflowStepRepresentation> steps = aggregatedStep.getSteps();
|
||||
assertThat(steps, hasSize(2));
|
||||
assertStep(steps, SetUserAttributeStepProviderFactory.ID, a -> {
|
||||
assertNotNull(a.getConfig());
|
||||
assertThat(a.getConfig().isEmpty(), is(false));
|
||||
assertThat(a.getConfig(), hasEntry("priority", List.of("1")));
|
||||
assertThat(a.getConfig(), hasEntry("message", List.of("message")));
|
||||
});
|
||||
assertAction(actions, DisableUserActionProviderFactory.ID, a -> {
|
||||
assertStep(steps, DisableUserStepProviderFactory.ID, a -> {
|
||||
assertNotNull(a.getConfig());
|
||||
assertThat(a.getConfig().isEmpty(), is(false));
|
||||
assertThat(a.getConfig(), hasEntry("priority", List.of("2")));
|
||||
@@ -83,18 +83,18 @@ public class AggregatedActionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionRun() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID)
|
||||
public void testStepRun() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withActions(ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(DisableUserActionProviderFactory.ID)
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build()).close();
|
||||
@@ -103,11 +103,11 @@ public class AggregatedActionTest {
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
assertFalse(user.isEnabled());
|
||||
@@ -137,8 +137,8 @@ public class AggregatedActionTest {
|
||||
return representation;
|
||||
}
|
||||
|
||||
private void assertAction(List<ResourcePolicyActionRepresentation> actions, String expectedProviderId, Consumer<ResourcePolicyActionRepresentation> assertions) {
|
||||
assertTrue(actions.stream()
|
||||
private void assertStep(List<WorkflowStepRepresentation> steps, String expectedProviderId, Consumer<WorkflowStepRepresentation> assertions) {
|
||||
assertTrue(steps.stream()
|
||||
.anyMatch(a -> {
|
||||
if (a.getProviderId().equals(expectedProviderId)) {
|
||||
assertions.accept(a);
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
@@ -40,19 +40,19 @@ import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.DeleteUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider;
|
||||
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
|
||||
import org.keycloak.models.workflow.DeleteUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
@@ -79,8 +79,8 @@ import org.openqa.selenium.WebDriver;
|
||||
|
||||
/**
|
||||
*/
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class BrokeredUserSessionRefreshTimeWorkflowTest {
|
||||
|
||||
private static final String REALM_NAME = "consumer";
|
||||
|
||||
@@ -124,52 +124,52 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
private static final String CLIENT_SECRET = "secret";
|
||||
|
||||
@Test
|
||||
public void testInvalidatePolicyOnIdentityProviderRemoval() {
|
||||
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
public void testInvalidateWorkflowOnIdentityProviderRemoval() {
|
||||
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(IdentityProviderPolicyConditionFactory.ID)
|
||||
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(IdentityProviderWorkflowConditionFactory.ID)
|
||||
.withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = consumerRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
List<WorkflowRepresentation> workflows = consumerRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
|
||||
ResourcePolicyRepresentation policyRep = consumerRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation();
|
||||
assertThat(policyRep.getConfig().getFirst("enabled"), nullValue());
|
||||
WorkflowRepresentation workflowRep = consumerRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation();
|
||||
assertThat(workflowRep.getConfig().getFirst("enabled"), nullValue());
|
||||
|
||||
// remove IDP
|
||||
consumerRealm.admin().identityProviders().get(IDP_OIDC_ALIAS).remove();
|
||||
|
||||
// create new user - it will trigger an activation event and therefore should disable the policy
|
||||
// create new user - it will trigger an activation event and therefore should disable the workflow
|
||||
consumerRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close();
|
||||
|
||||
// check the policy is disabled
|
||||
policyRep = consumerRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation();
|
||||
assertThat(policyRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false")));
|
||||
List<String> validationErrors = policyRep.getConfig().get("validation_error");
|
||||
// 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)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tesRunActionOnFederatedUser() {
|
||||
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
public void tesRunStepOnFederatedUser() {
|
||||
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(IdentityProviderPolicyConditionFactory.ID)
|
||||
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(IdentityProviderWorkflowConditionFactory.ID)
|
||||
.withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build()).close();
|
||||
@@ -184,16 +184,16 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
assertNotNull(user);
|
||||
assertTrue(user.isEnabled());
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertNull(user);
|
||||
} finally {
|
||||
@@ -202,7 +202,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
}));
|
||||
|
||||
// now authenticate with bob directly in the consumer realm - he is not associated with the IDP and thus not influenced
|
||||
// by the idp-exclusive lifecycle policy.
|
||||
// by the idp-exclusive lifecycle workflow.
|
||||
consumerRealmOAuth.openLoginForm();
|
||||
loginPage.fillLogin(bobFromConsumerRealm.getUsername(), bobFromConsumerRealm.getPassword());
|
||||
loginPage.submit();
|
||||
@@ -210,10 +210,10 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
// run the scheduled tasks - bob should not be affected.
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "bob");
|
||||
assertNotNull(user);
|
||||
assertTrue(user.isEnabled());
|
||||
@@ -221,7 +221,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
try {
|
||||
// run with a time offset - bob should still not be affected.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "bob");
|
||||
assertNotNull(user);
|
||||
} finally {
|
||||
@@ -231,16 +231,16 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddRemoveFedIdentityAffectsPolicyAssociation() {
|
||||
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
public void testAddRemoveFedIdentityAffectsWorkflowAssociation() {
|
||||
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.toString())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(IdentityProviderPolicyConditionFactory.ID)
|
||||
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(IdentityProviderWorkflowConditionFactory.ID)
|
||||
.withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build()).close();
|
||||
@@ -249,38 +249,38 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
ResourcePolicy policy = manager.getPolicies().get(0);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
Workflow workflow = manager.getWorkflows().get(0);
|
||||
UserModel alice = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(alice);
|
||||
|
||||
// alice should be associated with the policy
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), alice.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + alice.getUsername());
|
||||
// alice should be associated with the workflow
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
WorkflowStateProvider.ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), alice.getId());
|
||||
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + alice.getUsername());
|
||||
});
|
||||
|
||||
// remove the federated identity - alice should be disassociated from the policy and thus not deleted
|
||||
// remove the federated identity - alice should be disassociated from the workflow and thus not deleted
|
||||
UserRepresentation aliceInConsumerRealm = consumerRealm.admin().users().search(aliceFromProviderRealm.getUsername()).get(0);
|
||||
assertNotNull(aliceInConsumerRealm);
|
||||
consumerRealm.admin().users().get(aliceInConsumerRealm.getId()).removeFederatedIdentity(IDP_OIDC_ALIAS);
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// run with a time offset - alice should not be deleted as she is no longer associated with the IDP and thus the policy
|
||||
// run with a time offset - alice should not be deleted as she is no longer associated with the IDP and thus the workflow
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user, "User alice should not be deleted as she is no longer associated with the IDP and thus the policy.");
|
||||
assertNotNull(user, "User alice should not be deleted as she is no longer associated with the IDP and thus the workflow.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// add a federated identity for user bob - bob should now be associated with the policy and thus deleted when the scheduled tasks run
|
||||
// add a federated identity for user bob - bob should now be associated with the workflow and thus deleted when the scheduled tasks run
|
||||
FederatedIdentityRepresentation federatedIdentityRepresentation = new FederatedIdentityRepresentation();
|
||||
federatedIdentityRepresentation.setIdentityProvider(IDP_OIDC_ALIAS);
|
||||
federatedIdentityRepresentation.setUserId("bob-federated-id");
|
||||
@@ -289,12 +289,12 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// run with a time offset - bob should be deleted as he is now associated with the IDP and thus with the policy
|
||||
// run with a time offset - bob should be deleted as he is now associated with the IDP and thus with the workflow
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "bob");
|
||||
assertNull(user);
|
||||
} finally {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
@@ -15,23 +15,22 @@ import java.util.List;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
@@ -44,8 +43,8 @@ import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class GroupMembershipJoinPolicyTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class GroupMembershipJoinWorkflowTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -67,24 +66,24 @@ public class GroupMembershipJoinPolicyTest {
|
||||
groupId = ApiUtil.getCreatedId(response);
|
||||
}
|
||||
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.GROUP_MEMBERSHIP_JOIN.name())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(GroupMembershipPolicyConditionFactory.ID)
|
||||
.withConfig(GroupMembershipPolicyConditionFactory.EXPECTED_GROUPS, groupId)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(GroupMembershipWorkflowConditionFactory.ID)
|
||||
.withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("attribute", "attr1")
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
try (Response response = workflows.create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
@@ -101,14 +100,12 @@ public class GroupMembershipJoinPolicyTest {
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
// set offset to 7 days - notify step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
@@ -127,35 +124,35 @@ public class GroupMembershipJoinPolicyTest {
|
||||
groupId = ApiUtil.getCreatedId(response);
|
||||
}
|
||||
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(GroupMembershipPolicyConditionFactory.ID)
|
||||
.withConfig(GroupMembershipPolicyConditionFactory.EXPECTED_GROUPS, groupId)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(GroupMembershipWorkflowConditionFactory.ID)
|
||||
.withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
List<ResourcePolicyRepresentation> policies = managedRealm.admin().resources().policies().list();
|
||||
assertThat(policies, hasSize(1));
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
|
||||
ResourcePolicyRepresentation policyRep = managedRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation();
|
||||
assertThat(policyRep.getConfig().getFirst("enabled"), nullValue());
|
||||
WorkflowRepresentation workflowRep = managedRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation();
|
||||
assertThat(workflowRep.getConfig().getFirst("enabled"), nullValue());
|
||||
|
||||
// remove group
|
||||
managedRealm.admin().groups().group(groupId).remove();
|
||||
|
||||
// create new user - it will trigger an activation event and therefore should disable the policy
|
||||
// create new user - it will trigger an activation event and therefore should disable the workflow
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close();
|
||||
|
||||
// check the policy is disabled
|
||||
policyRep = managedRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation();
|
||||
assertThat(policyRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false")));
|
||||
List<String> validationErrors = policyRep.getConfig().get("validation_error");
|
||||
// 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)));
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.models.policy.conditions.RolePolicyConditionFactory.EXPECTED_ROLES;
|
||||
import static org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory.EXPECTED_ROLES;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
@@ -15,22 +15,22 @@ import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.RolePolicyConditionFactory;
|
||||
import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
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.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
@@ -43,8 +43,8 @@ import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class RolePolicyConditionTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class RoleWorkflowConditionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -64,7 +64,7 @@ public class RolePolicyConditionTest {
|
||||
@Test
|
||||
public void testConditionForSingleRole() {
|
||||
String expected = "realm-role-1";
|
||||
createPolicy(expected);
|
||||
createWorkflow(expected);
|
||||
assertUserRoles("user-1", false);
|
||||
assertUserRoles("user-2", false, "not-valid-role");
|
||||
assertUserRoles("user-3", true, expected);
|
||||
@@ -73,7 +73,7 @@ public class RolePolicyConditionTest {
|
||||
@Test
|
||||
public void testConditionForMultipleRole() {
|
||||
List<String> expected = List.of("realm-role-1", "realm-role-2", "client-a/client-role-1");
|
||||
createPolicy(expected);
|
||||
createWorkflow(expected);
|
||||
assertUserRoles("user-1", false, List.of("realm-role-1", "realm-role-2"));
|
||||
assertUserRoles("user-2", false, List.of("realm-role-1", "realm-role-2", "client-b/client-role-1"));
|
||||
assertUserRoles("user-3", true, expected);
|
||||
@@ -105,9 +105,9 @@ public class RolePolicyConditionTest {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
// set offset to 7 days - notify step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
new ResourcePolicyManager(session).runScheduledActions();
|
||||
new WorkflowsManager(session).runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
@@ -123,38 +123,38 @@ public class RolePolicyConditionTest {
|
||||
}));
|
||||
}
|
||||
|
||||
private void createPolicy(String... expectedValues) {
|
||||
createPolicy(Map.of(EXPECTED_ROLES, List.of(expectedValues)));
|
||||
private void createWorkflow(String... expectedValues) {
|
||||
createWorkflow(Map.of(EXPECTED_ROLES, List.of(expectedValues)));
|
||||
}
|
||||
|
||||
private void createPolicy(List<String> expectedValues) {
|
||||
createPolicy(Map.of(EXPECTED_ROLES, expectedValues));
|
||||
private void createWorkflow(List<String> expectedValues) {
|
||||
createWorkflow(Map.of(EXPECTED_ROLES, expectedValues));
|
||||
}
|
||||
|
||||
private void createPolicy(Map<String, List<String>> attributes) {
|
||||
private void createWorkflow(Map<String, List<String>> attributes) {
|
||||
for (String roleName : attributes.getOrDefault(EXPECTED_ROLES, List.of())) {
|
||||
createRoleIfNotExists(roleName);
|
||||
}
|
||||
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.ROLE_GRANTED.name())
|
||||
.recurring()
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(RolePolicyConditionFactory.ID)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(RoleWorkflowConditionFactory.ID)
|
||||
.withConfig(attributes)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("notified", "true")
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
try (Response response = workflows.create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@@ -32,14 +32,14 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyActionRunnerSuccessEvent;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowStepRunnerSuccessEvent;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.storage.UserStoragePrivateUtil;
|
||||
import org.keycloak.testframework.annotations.InjectAdminClient;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
@@ -47,8 +47,8 @@ import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMScheduledTaskServerConfig.class)
|
||||
public class ActionRunnerScheduledTaskTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsScheduledTaskServerConfig.class)
|
||||
public class StepRunnerScheduledTaskTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ActionRunnerScheduledTaskTest {
|
||||
Keycloak adminClient;
|
||||
|
||||
@Test
|
||||
public void testActionRunnerScheduledTask() {
|
||||
public void testStepRunnerScheduledTask() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
RealmRepresentation realm = new RealmRepresentation();
|
||||
|
||||
@@ -68,21 +68,21 @@ public class ActionRunnerScheduledTaskTest {
|
||||
|
||||
adminClient.realms().create(realm);
|
||||
|
||||
assertActionRuns(realm.getRealm());
|
||||
assertStepRuns(realm.getRealm());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertActionRuns(String realmName) {
|
||||
private void assertStepRuns(String realmName) {
|
||||
RealmResource realm = adminClient.realm(realmName);
|
||||
|
||||
realm.resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(SetUserAttributeActionProviderFactory.ID)
|
||||
realm.workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
@@ -99,8 +99,8 @@ public class ActionRunnerScheduledTaskTest {
|
||||
CountDownLatch count = new CountDownLatch(2);
|
||||
|
||||
ProviderEventListener listener = event -> {
|
||||
if (event instanceof ResourcePolicyActionRunnerSuccessEvent e) {
|
||||
KeycloakSession s = e.getSession();
|
||||
if (event instanceof WorkflowStepRunnerSuccessEvent e) {
|
||||
KeycloakSession s = e.session();
|
||||
RealmModel r = s.getContext().getRealm();
|
||||
|
||||
if (!realmName.equals(r.getName())) {
|
||||
@@ -112,7 +112,7 @@ public class ActionRunnerScheduledTaskTest {
|
||||
if (user.isEnabled() && user.getAttributes().containsKey("message")) {
|
||||
// notified
|
||||
count.countDown();
|
||||
// force execution of next action
|
||||
// force execution of next step
|
||||
user.removeAttribute("message");
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(20).toSeconds()));
|
||||
} else if (!user.isEnabled()) {
|
||||
@@ -125,9 +125,9 @@ public class ActionRunnerScheduledTaskTest {
|
||||
try {
|
||||
sessionFactory.register(listener);
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
System.out.println("Waiting for actions to be run for realm " + realmName);
|
||||
System.out.println("Waiting for steps to be run for realm " + realmName);
|
||||
assertTrue(count.await(15, TimeUnit.SECONDS));
|
||||
System.out.println("... actions run for realm " + realmName);
|
||||
System.out.println("... steps run for realm " + realmName);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
@@ -15,19 +15,19 @@ import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.UserAttributePolicyConditionFactory;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory;
|
||||
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.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
@@ -38,8 +38,8 @@ import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class UserAttributePolicyConditionTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class UserAttributeWorkflowConditionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -59,7 +59,7 @@ public class UserAttributePolicyConditionTest {
|
||||
@Test
|
||||
public void testConditionForSingleValuedAttribute() {
|
||||
String expected = "valid";
|
||||
createPolicy(expected);
|
||||
createWorkflow(expected);
|
||||
assertUserAttribute("user-1", false);
|
||||
assertUserAttribute("user-2", false, "not-valid");
|
||||
assertUserAttribute("user-3", true, expected);
|
||||
@@ -68,7 +68,7 @@ public class UserAttributePolicyConditionTest {
|
||||
@Test
|
||||
public void testConditionForMultiValuedAttribute() {
|
||||
List<String> expected = List.of("v1", "v2", "v3");
|
||||
createPolicy(expected);
|
||||
createWorkflow(expected);
|
||||
assertUserAttribute("user-1", false, "v1");
|
||||
assertUserAttribute("user-2", true, expected);
|
||||
assertUserAttribute("user-3", false, "v1", "v2", "v3", "v4");
|
||||
@@ -77,7 +77,7 @@ public class UserAttributePolicyConditionTest {
|
||||
@Test
|
||||
public void testConditionForMultipleAttributes() {
|
||||
Map<String, List<String>> expected = Map.of("a", List.of("a1"), "b", List.of("b1"), "c", List.of("c11", "c2"));
|
||||
createPolicy(expected);
|
||||
createWorkflow(expected);
|
||||
assertUserAttribute("user-1", false, Map.of("a", List.of("a3"), "b", List.of("b1"), "c", List.of("c1", "c2")));
|
||||
assertUserAttribute("user-2", true, expected);
|
||||
assertUserAttribute("user-3", false, Map.of("a", List.of("a1"), "b", List.of("b1")));
|
||||
@@ -104,9 +104,9 @@ public class UserAttributePolicyConditionTest {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
// set offset to 7 days - notify step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
new ResourcePolicyManager(session).runScheduledActions();
|
||||
new WorkflowsManager(session).runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
@@ -122,34 +122,34 @@ public class UserAttributePolicyConditionTest {
|
||||
}));
|
||||
}
|
||||
|
||||
private void createPolicy(String... expectedValues) {
|
||||
createPolicy(Map.of("attribute", List.of(expectedValues)));
|
||||
private void createWorkflow(String... expectedValues) {
|
||||
createWorkflow(Map.of("attribute", List.of(expectedValues)));
|
||||
}
|
||||
|
||||
private void createPolicy(List<String> expectedValues) {
|
||||
createPolicy(Map.of("attribute", expectedValues));
|
||||
private void createWorkflow(List<String> expectedValues) {
|
||||
createWorkflow(Map.of("attribute", expectedValues));
|
||||
}
|
||||
|
||||
private void createPolicy(Map<String, List<String>> attributes) {
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
private void createWorkflow(Map<String, List<String>> attributes) {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.name())
|
||||
.recurring()
|
||||
.onConditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(UserAttributePolicyConditionFactory.ID)
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(UserAttributeWorkflowConditionFactory.ID)
|
||||
.withConfig(attributes)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("notified", "true")
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
try (Response response = workflows.create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -6,14 +6,14 @@ import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
@@ -35,10 +35,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient;
|
||||
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailByRecipient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class UserCreationTimePolicyTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class UserCreationTimeWorkflowTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -62,40 +62,40 @@ public class UserCreationTimePolicyTest {
|
||||
|
||||
@Test
|
||||
public void testDisableUserBasedOnCreationDate() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - this will trigger the association with the policy
|
||||
// create a new user - this will trigger the association with the workflow
|
||||
managedRealm.admin().users().create(
|
||||
this.getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"));
|
||||
this.getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org")).close();
|
||||
|
||||
// test running the scheduled actions
|
||||
// test running the scheduled steps
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
// running the scheduled tasks now shouldn't pick up any action as none are due to run yet
|
||||
manager.runScheduledActions();
|
||||
// running the scheduled tasks now shouldn't pick up any step as none are due to run yet
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
// set offset to 7 days - notify step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
assertTrue(user.isEnabled());
|
||||
} finally {
|
||||
@@ -103,27 +103,27 @@ public class UserCreationTimePolicyTest {
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify that the notify action was executed by checking email was sent
|
||||
// Verify that the notify step was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "alice@wornderland.org");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
assertNotNull(testUserMessage, "The first step (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// logging-in with alice should not reset the policy - we should still run the disable action next
|
||||
// logging-in with alice should not reset the workflow - we should still run the disable step next
|
||||
oauth.openLoginForm();
|
||||
loginPage.fillLogin("alice", "alice");
|
||||
loginPage.submit();
|
||||
assertTrue(driver.getPageSource().contains("Happy days"));
|
||||
|
||||
// test running the scheduled actions
|
||||
// test running the scheduled steps
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// set offset to 11 days - disable action should run now
|
||||
// set offset to 11 days - disable step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertFalse(user.isEnabled());
|
||||
} finally {
|
||||
@@ -15,15 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailsByRecipient;
|
||||
import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.verifyEmailContent;
|
||||
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailByRecipient;
|
||||
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailsByRecipient;
|
||||
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.verifyEmailContent;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
@@ -36,13 +36,13 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
@@ -62,8 +62,8 @@ import org.keycloak.testframework.ui.annotations.InjectWebDriver;
|
||||
import org.keycloak.testframework.ui.page.LoginPage;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class UserSessionRefreshTimePolicyTest {
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class UserSessionRefreshTimeWorkflowTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@@ -93,49 +93,49 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
oauth.realm("default");
|
||||
|
||||
runOnServer.run(session -> {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
manager.removePolicies();
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
manager.removeWorkflows();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisabledUserAfterInactivityPeriod() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID)
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// login with alice - this will attach the policy to the user and schedule the first action
|
||||
// login with alice - this will attach the workflow to the user and schedule the first step
|
||||
oauth.openLoginForm();
|
||||
String username = userAlice.getUsername();
|
||||
loginPage.fillLogin(username, userAlice.getPassword());
|
||||
loginPage.submit();
|
||||
assertTrue(driver.getPageSource() != null && driver.getPageSource().contains("Happy days"));
|
||||
|
||||
// test running the scheduled actions
|
||||
// test running the scheduled steps
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
|
||||
// running the scheduled tasks now shouldn't pick up any action as none are due to run yet
|
||||
manager.runScheduledActions();
|
||||
// running the scheduled tasks now shouldn't pick up any step as none are due to run yet
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
|
||||
try {
|
||||
// set offset to 6 days - notify action should run now
|
||||
// set offset to 6 days - notify step should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
} finally {
|
||||
@@ -143,22 +143,22 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
}));
|
||||
|
||||
// Verify that the notify action was executed by checking email was sent
|
||||
// Verify that the notify step was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "master-admin@email.org");
|
||||
assertNotNull(testUserMessage, "The first action (notify) should have sent an email.");
|
||||
assertNotNull(testUserMessage, "The first step (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// trigger a login event that should reset the flow of the policy
|
||||
// trigger a login event that should reset the flow of the workflow
|
||||
oauth.openLoginForm();
|
||||
|
||||
runOnServer.run((session -> {
|
||||
try {
|
||||
// setting the offset to 11 days should not run the second action as we re-started the flow on login
|
||||
// setting the offset to 11 days should not run the second step as we re-started the flow on login
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
manager.runScheduledActions();
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
} finally {
|
||||
@@ -166,13 +166,13 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
|
||||
try {
|
||||
// first action has run and the next action should be triggered after 5 more days (time difference between the actions)
|
||||
// first step has run and the next step should be triggered after 5 more days (time difference between the steps)
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(17).toSeconds()));
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
manager.runScheduledActions();
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
// second action should have run and the user should be disabled now
|
||||
// second step should have run and the user should be disabled now
|
||||
assertFalse(user.isEnabled());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
@@ -181,27 +181,27 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiplePolicies() {
|
||||
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
|
||||
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
public void testMultipleWorkflows() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("custom_subject_key", "notifier1_subject")
|
||||
.withConfig("custom_message", "notifier1_message")
|
||||
.build()
|
||||
).of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
|
||||
).of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.withConfig("custom_subject_key", "notifier2_subject")
|
||||
.withConfig("custom_message", "notifier2_message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
// perform a login to associate the policies with the new user.
|
||||
// perform a login to associate the workflows with the new user.
|
||||
oauth.openLoginForm();
|
||||
String username = userAlice.getUsername();
|
||||
loginPage.fillLogin(username, userAlice.getPassword());
|
||||
@@ -210,7 +210,7 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
UserProvider users = session.users();
|
||||
UserModel user = users.getUserByUsername(realm, username);
|
||||
@@ -218,7 +218,7 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = users.getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
} finally {
|
||||
@@ -226,23 +226,23 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first notify action was executed by checking email was sent
|
||||
// Verify that the first notify step was executed by checking email was sent
|
||||
List<MimeMessage> testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org");
|
||||
// Only one notify message should be sent
|
||||
assertEquals(1, testUserMessages.size());
|
||||
assertNotNull(testUserMessages.get(0), "The first action (notify) should have sent an email.");
|
||||
assertNotNull(testUserMessages.get(0), "The first step (notify) should have sent an email.");
|
||||
verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier1_subject", "notifier1_message");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
|
||||
manager.runScheduledActions();
|
||||
manager.runScheduledSteps();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertTrue(user.isEnabled());
|
||||
} finally {
|
||||
@@ -250,11 +250,11 @@ public class UserSessionRefreshTimePolicyTest {
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the second notify action was executed by checking email was sent
|
||||
// Verify that the second notify step was executed by checking email was sent
|
||||
testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org");
|
||||
// Only one notify message should be sent
|
||||
assertEquals(1, testUserMessages.size());
|
||||
assertNotNull(testUserMessages.get(0), "The second action (notify) should have sent an email.");
|
||||
assertNotNull(testUserMessages.get(0), "The second step (notify) should have sent an email.");
|
||||
verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier2_subject", "notifier2_message");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.hamcrest.Matchers;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.workflow.DeleteUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowStep;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.mail.MailServer;
|
||||
import org.keycloak.testframework.mail.annotations.InjectMailServer;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.tests.utils.MailUtils;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class WorkflowManagementTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectMailServer
|
||||
private MailServer mailServer;
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
try (Response response = workflows.create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(1));
|
||||
|
||||
assertThat(actualWorkflows.get(0).getProviderId(), 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWithNoConditions() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
expectedWorkflows.get(0).setConditions(null);
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
workflows.create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.recurring()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).of(EventBasedWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.LOGIN.toString())
|
||||
.recurring()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the workflow and setup the only step in the workflow
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(2));
|
||||
|
||||
WorkflowRepresentation workflow = actualWorkflows.stream().filter(p -> UserCreationTimeWorkflowProviderFactory.ID.equals(p.getProviderId())).findAny().orElse(null);
|
||||
assertThat(workflow, notNullValue());
|
||||
String id = workflow.getId();
|
||||
workflows.workflow(id).delete().close();
|
||||
actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(1));
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
List<ScheduledStep> steps = stateProvider.getScheduledStepsByWorkflow(id);
|
||||
assertTrue(steps.isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() {
|
||||
List<WorkflowRepresentation> expectedWorkflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.name("test-workflow")
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
|
||||
try (Response response = workflows.create(expectedWorkflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(1));
|
||||
WorkflowRepresentation workflow = actualWorkflows.get(0);
|
||||
assertThat(workflow.getName(), is("test-workflow"));
|
||||
|
||||
workflow.setName("changed");
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close();
|
||||
actualWorkflows = workflows.list();
|
||||
workflow = actualWorkflows.get(0);
|
||||
assertThat(workflow.getName(), is("changed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkflowDoesNotFallThroughStepsInSingleRun() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the workflow and setup the first step
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm,"testuser");
|
||||
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
assertEquals(2, manager.getSteps(workflow.getId()).size());
|
||||
WorkflowStep notifyStep = manager.getSteps(workflow.getId()).get(0);
|
||||
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId());
|
||||
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(notifyStep.getId(), scheduledStep.stepId());
|
||||
|
||||
try {
|
||||
// Simulate the user being 12 days old, making them eligible for both steps' time conditions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
|
||||
// Verify that the next step was scheduled for the user
|
||||
WorkflowStep disableStep = manager.getSteps(workflow.getId()).get(1);
|
||||
scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId());
|
||||
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(disableStep.getId(), scheduledStep.stepId(), "The second step should have been scheduled");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first step (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first step (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignWorkflowToExistingResources() {
|
||||
// create some realm users
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build()).close();
|
||||
}
|
||||
|
||||
// create some users associated with a federated identity
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "idp-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
|
||||
idp.setAlias("someidp");
|
||||
idp.setProviderId(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID);
|
||||
idp.setEnabled(true);
|
||||
managedRealm.admin().identityProviders().create(idp).close();
|
||||
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.name())
|
||||
.onConditions(WorkflowConditionRepresentation.create()
|
||||
.of(IdentityProviderWorkflowConditionFactory.ID)
|
||||
.withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, "someidp")
|
||||
.build())
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// now with the workflow in place, let's create a couple more idp users - these will be attached to the workflow on
|
||||
// creation.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "new-idp-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
// new realm users created after the workflow - these should not be attached to the workflow because they are not idp users.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-user-" + i).build()).close();
|
||||
}
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager workflowsManager = new WorkflowsManager(session);
|
||||
List<Workflow> registeredWorkflows = workflowsManager.getWorkflows();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
|
||||
assertEquals(2, workflowsManager.getSteps(workflow.getId()).size());
|
||||
WorkflowStep notifyStep = workflowsManager.getSteps(workflow.getId()).get(0);
|
||||
|
||||
// check no workflows are yet attached to the previous users, only to the ones created after the workflow was in place
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertEquals(3, scheduledSteps.size());
|
||||
scheduledSteps.forEach(scheduledStep -> {
|
||||
assertEquals(notifyStep.getId(), scheduledStep.stepId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledStep.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
try {
|
||||
// let's run the schedule steps for the new users so they transition to the next one.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
workflowsManager.runScheduledSteps();
|
||||
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowStep disableStep = workflowsManager.getSteps(workflow.getId()).get(1);
|
||||
scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertEquals(3, scheduledSteps.size());
|
||||
scheduledSteps.forEach(scheduledStep -> {
|
||||
assertEquals(disableStep.getId(), scheduledStep.stepId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledStep.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
// assign the workflow to the eligible users - i.e. only users from the same idp who are not yet assigned to the workflow.
|
||||
workflowsManager.scheduleAllEligibleResources(workflow);
|
||||
|
||||
// check workflow was correctly assigned to the old users, not affecting users already associated with the workflow.
|
||||
scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertEquals(13, scheduledSteps.size());
|
||||
|
||||
List<ScheduledStep> scheduledToNotify = scheduledSteps.stream()
|
||||
.filter(step -> notifyStep.getId().equals(step.stepId())).toList();
|
||||
assertEquals(10, scheduledToNotify.size());
|
||||
scheduledToNotify.forEach(scheduledStep -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledStep.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("idp-user-"));
|
||||
});
|
||||
|
||||
List<ScheduledStep> scheduledToDisable = scheduledSteps.stream()
|
||||
.filter(step -> disableStep.getId().equals(step.stepId())).toList();
|
||||
assertEquals(3, scheduledToDisable.size());
|
||||
scheduledToDisable.forEach(scheduledStep -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledStep.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableWorkflow() {
|
||||
// create a test workflow
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.name("test-workflow")
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
WorkflowsResource workflows = managedRealm.admin().workflows();
|
||||
List<WorkflowRepresentation> actualWorkflows = workflows.list();
|
||||
assertThat(actualWorkflows, Matchers.hasSize(1));
|
||||
WorkflowRepresentation workflow = actualWorkflows.get(0);
|
||||
assertThat(workflow.getName(), is("test-workflow"));
|
||||
|
||||
// create a new user - should bind the user to the workflow and setup the first step
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// Advance time so the user is eligible for the first step, then run the scheduled steps so they transition to the next one.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
assertTrue(user.isEnabled(), "The second step (disable) should NOT have run.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first step (notify) was executed by checking email was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com");
|
||||
assertNotNull(testUserMessage, "The first step (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
|
||||
// disable the workflow - scheduled steps should be paused and workflow should not activate for new users
|
||||
workflow.getConfig().putSingle("enabled", "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
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("anotheruser").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
List<Workflow> registeredWorkflow = manager.getWorkflows();
|
||||
assertEquals(1, registeredWorkflow.size());
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0));
|
||||
|
||||
// verify that there's only one scheduled step, for the first user
|
||||
assertEquals(1, scheduledSteps.size());
|
||||
UserModel scheduledStepUser = session.users().getUserById(realm, scheduledSteps.get(0).resourceId());
|
||||
assertNotNull(scheduledStepUser);
|
||||
assertTrue(scheduledStepUser.getUsername().startsWith("testuser"));
|
||||
|
||||
try {
|
||||
// Advance time so the first user would be eligible for the second step, then run the scheduled steps.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that the step was NOT executed as the workflow is disabled.
|
||||
assertTrue(user.isEnabled(), "The second step (disable) should NOT have run as the workflow is disabled.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// re-enable the workflow - scheduled steps should resume and new users should be bound to the workflow
|
||||
workflow.getConfig().putSingle("enabled", "true");
|
||||
managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close();
|
||||
|
||||
// create a third user - should bind the user to the workflow as it is enabled again
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").email("thirduser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// Advance time so the first user would be eligible for the second step, and third user would be eligible for the first step, then run the scheduled steps.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
// Verify that the step was executed as the workflow was re-enabled.
|
||||
assertFalse(user.isEnabled(), "The second step (disable) should have run as the workflow was re-enabled.");
|
||||
|
||||
// Verify that the third user was bound to the workflow
|
||||
user = session.users().getUserByUsername(realm, "thirduser");
|
||||
assertTrue(user.isEnabled(), "The second step (disable) should NOT have run");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the first step (notify) was executed by checking email was sent
|
||||
testUserMessage = findEmailByRecipient(mailServer, "thirduser@example.com");
|
||||
assertNotNull(testUserMessage, "The first step (notify) should have sent an email.");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecurringWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.recurring()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should bind the user to the workflow and setup the only step in the workflow
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close();
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
Workflow workflow = manager.getWorkflows().get(0);
|
||||
WorkflowStep step = manager.getSteps(workflow.getId()).get(0);
|
||||
|
||||
// Verify that the step was scheduled again for the user
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId());
|
||||
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(step.getId(), scheduledStep.stepId(), "The step should have been scheduled again");
|
||||
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that there should be two emails sent
|
||||
assertEquals(2, findEmailsByRecipient(mailServer, "testuser@example.com").size());
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunImmediateWorkflow() {
|
||||
// create a test workflow with no time conditions - should run immediately when scheduled
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(2))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
// create a new user - should be bound to the new workflow and all steps should run right away
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build()).close();
|
||||
|
||||
// check the user has the attribute set and is disabled
|
||||
runOnServer.run(session -> {
|
||||
configureSessionContext(session);
|
||||
UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "testuser");
|
||||
assertEquals("message", user.getAttributes().get("message").get(0));
|
||||
assertFalse(user.isEnabled());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserStepSendsEmailWithDefaultDisableMessage() {
|
||||
// Create workflow: disable at 10 days, notify 3 days before (at day 7)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(3))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("test@example.com").name("John", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 7 days old (eligible for notify step)
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
|
||||
|
||||
manager.runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test@example.com");
|
||||
verifyEmailContent(testUserMessage, "test@example.com", "Disable", "John", "3", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserStepSendsEmailWithDefaultDeleteMessage() {
|
||||
// Create workflow: delete at 30 days, notify 15 days before (at day 15)
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser2").email("test2@example.com").name("Jane", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 15 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test2@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test2@example.com");
|
||||
verifyEmailContent(testUserMessage, "test2@example.com", "Deletion", "Jane", "15", "inactivity", "permanently deleted");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserStepWithCustomMessageOverride() {
|
||||
// Create workflow: disable at 7 days, notify 2 days before (at day 5) with custom message
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("reason", "compliance requirement")
|
||||
.withConfig("custom_message", "Your account requires immediate attention due to new compliance policies.")
|
||||
.withConfig("custom_subject_key", "customComplianceSubject")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser3").email("test3@example.com").name("Bob", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
// Simulate user being 5 days old
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify email was sent to our test user
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test3@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for test3@example.com");
|
||||
verifyEmailContent(testUserMessage, "test3@example.com", "", "Bob", "2", "immediate attention due to new compliance policies");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyUserStepSkipsUsersWithoutEmailButLogsWarning() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
// But should still create state record for the workflow flow
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser4");
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
var scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId());
|
||||
assertEquals(1, scheduledSteps.size());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Should NOT send email to user without email address
|
||||
MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4");
|
||||
assertNull(testUserMessage, "No email should be sent to user without email address");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompleteUserLifecycleWithMultipleNotifications() {
|
||||
// Create workflow: just disable at 30 days with one notification before
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.withConfig("reason", "inactivity")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(15))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser5").email("testuser5@example.com").name("TestUser5", "").build()).close();
|
||||
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser5");
|
||||
|
||||
try {
|
||||
// Day 15: First notification - this should run the notify step and schedule the disable step
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
// Check that user is still enabled after notification
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertTrue(user.isEnabled(), "User should still be enabled after notification");
|
||||
|
||||
// Day 30 + 15 minutes: Disable user - run 15 minutes after the scheduled time to ensure it's due
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(30).toSeconds()) + Math.toIntExact(Duration.ofMinutes(15).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
|
||||
// Verify user is disabled
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
assertNotNull(user, "User should still exist after disable");
|
||||
assertFalse(user.isEnabled(), "User should be disabled");
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify notification was sent
|
||||
MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser5@example.com");
|
||||
assertNotNull(testUserMessage, "No email found for testuser5@example.com");
|
||||
verifyEmailContent(testUserMessage, "testuser5@example.com", "Disable", "TestUser5", "15", "inactivity");
|
||||
|
||||
mailServer.runCleanup();
|
||||
}
|
||||
|
||||
public static List<MimeMessage> findEmailsByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static MimeMessage findEmailByRecipient(MailServer mailServer, String expectedRecipient) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).equals(expectedRecipient);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private MimeMessage findEmailByRecipientContaining(String recipientPart) {
|
||||
return Arrays.stream(mailServer.getReceivedMessages())
|
||||
.filter(msg -> {
|
||||
try {
|
||||
return MailUtils.getRecipient(msg).contains(recipientPart);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
|
||||
public static void verifyEmailContent(MimeMessage message, String expectedRecipient, String subjectContains,
|
||||
String... contentContains) {
|
||||
try {
|
||||
assertEquals(expectedRecipient, MailUtils.getRecipient(message));
|
||||
assertThat(message.getSubject(), Matchers.containsString(subjectContains));
|
||||
|
||||
MailUtils.EmailBody body = MailUtils.getBody(message);
|
||||
String textContent = body.getText();
|
||||
String htmlContent = body.getHtml();
|
||||
|
||||
for (String expectedContent : contentContains) {
|
||||
boolean foundInText = textContent.contains(expectedContent);
|
||||
boolean foundInHtml = htmlContent.contains(expectedContent);
|
||||
assertTrue(foundInText || foundInHtml,
|
||||
"Email content should contain: " + expectedContent +
|
||||
"\nText: " + textContent +
|
||||
"\nHTML: " + htmlContent);
|
||||
}
|
||||
} catch (MessagingException | IOException e) {
|
||||
Assertions.fail("Failed to read email message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import org.keycloak.models.workflow.WorkflowsEventListenerFactory;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
public class WorkflowsScheduledTaskServerConfig extends WorkflowsServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config)
|
||||
.option("spi-events-listener--" + WorkflowsEventListenerFactory.ID + "--step-runner-task-interval", "1000");
|
||||
}
|
||||
}
|
||||
@@ -15,16 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
public class RLMServerConfig implements KeycloakServerConfig {
|
||||
public class WorkflowsServerConfig implements KeycloakServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return config.features(Feature.RESOURCE_LIFECYCLE);
|
||||
return config.features(Feature.WORKFLOWS);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user