diff --git a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java b/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java index c87f0d674cf..1bad736332a 100644 --- a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java @@ -1,10 +1,11 @@ package org.keycloak.representations.resources.policies; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; + +import org.keycloak.common.util.MultivaluedHashMap; public class ResourcePolicyActionRepresentation { @@ -16,7 +17,8 @@ public class ResourcePolicyActionRepresentation { private String id; private String providerId; - private Map> config; + private MultivaluedHashMap config; + private List actions; public ResourcePolicyActionRepresentation() { // reflection @@ -26,14 +28,19 @@ public class ResourcePolicyActionRepresentation { this(providerId, null); } - public ResourcePolicyActionRepresentation(String providerId, Map> config) { - this(null, providerId, config); + public ResourcePolicyActionRepresentation(String providerId, MultivaluedHashMap config) { + this(null, providerId, config, null); } - public ResourcePolicyActionRepresentation(String id, String providerId, Map> config) { + public ResourcePolicyActionRepresentation(String id, String providerId, MultivaluedHashMap config, List actions) { this.id = id; this.providerId = providerId; this.config = config; + this.actions = actions; + } + + public String getId() { + return id; } public String getProviderId() { @@ -44,25 +51,37 @@ public class ResourcePolicyActionRepresentation { this.providerId = providerId; } - public Map> getConfig() { + public MultivaluedHashMap getConfig() { return config; } - public void setConfig(Map> config) { + public void setConfig(MultivaluedHashMap config) { this.config = config; } public void setConfig(String key, String value) { + setConfig(key, Collections.singletonList(value)); + } + + public void setConfig(String key, List values) { if (this.config == null) { - this.config = new HashMap<>(); + this.config = new MultivaluedHashMap<>(); } - this.config.put(key, Collections.singletonList(value)); + this.config.put(key, values); } private void setAfter(long ms) { setConfig(AFTER_KEY, String.valueOf(ms)); } + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + public static class Builder { private ResourcePolicyActionRepresentation action; @@ -91,6 +110,16 @@ public class ResourcePolicyActionRepresentation { return this; } + public Builder withActions(ResourcePolicyActionRepresentation... actions) { + action.setActions(Arrays.asList(actions)); + return this; + } + + public Builder withConfig(String key, List values) { + action.setConfig(key, values); + return this; + } + public ResourcePolicyActionRepresentation build() { return action; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java index 4db0c532191..9bac4576e12 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java +++ b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java @@ -17,6 +17,8 @@ package org.keycloak.models.policy; +import java.util.List; + import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; @@ -28,6 +30,7 @@ public class ResourceAction implements Comparable { private String id; private String providerId; private MultivaluedHashMap config; + private List actions = List.of(); public ResourceAction() { // reflection @@ -37,9 +40,10 @@ public class ResourceAction implements Comparable { this.providerId = providerId; } - public ResourceAction(String providerId, MultivaluedHashMap config) { + public ResourceAction(String providerId, MultivaluedHashMap config, List actions) { this.providerId = providerId; this.config = config; + this.actions = actions; } public ResourceAction(ComponentModel model) { @@ -96,6 +100,17 @@ public class ResourceAction implements Comparable { return Long.valueOf(getConfig().getFirstOrDefault(AFTER_KEY, "0")); } + public List getActions() { + if (actions == null) { + return List.of(); + } + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + @Override public int compareTo(ResourceAction other) { return Integer.compare(this.getPriority(), other.getPriority()); diff --git a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java b/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java new file mode 100644 index 00000000000..229fb7ba804 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java @@ -0,0 +1,47 @@ +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 userIds) { + ResourcePolicyManager manager = new ResourcePolicyManager(session); + List 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; + } +} diff --git a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java b/services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java new file mode 100644 index 00000000000..a884c533160 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java @@ -0,0 +1,54 @@ +package org.keycloak.models.policy; + +import java.util.List; + +import org.keycloak.Config; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +public class AggregatedActionProviderFactory implements ResourceActionProviderFactory { + + public static final String ID = "aggregated-action-provider"; + + @Override + public AggregatedActionProvider create(KeycloakSession session, ComponentModel model) { + return new AggregatedActionProvider(session, model); + } + + @Override + public void init(Config.Scope config) { + // no-op + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return ID; + } + + @Override + public ResourceType getType() { + return ResourceType.USERS; + } + + @Override + public String getHelpText() { + return ""; + } + + @Override + public List getConfigProperties() { + return List.of(); + } +} diff --git a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java index d20a7b3dc65..25e3e994e05 100644 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java +++ b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java @@ -17,14 +17,20 @@ package org.keycloak.models.policy; -import jakarta.ws.rs.BadRequestException; +import static java.util.Optional.ofNullable; + import java.time.Duration; +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.Set; import java.util.stream.Collectors; +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; @@ -35,12 +41,15 @@ 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 ResourcePolicyStateProvider policyStateProvider; + private final ResourcePolicyStateProvider policyStateProvider; public static boolean isFeatureEnabled() { return Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE); @@ -101,7 +110,7 @@ public class ResourcePolicyManager { .collect(Collectors.toSet()); // get the stable IDs of the old actions - List oldActions = getActions(policy); + List oldActions = getActions(policy.getId()); Set oldActionIds = oldActions.stream() .map(ResourceAction::getId) .collect(Collectors.toSet()); @@ -126,14 +135,23 @@ public class ResourcePolicyManager { // assign priority based on index. action.setPriority(i + 1); + List subActions = Optional.ofNullable(action.getActions()).orElse(List.of()); + // persist the new action component. - addAction(policy, action); + 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(ResourcePolicy policy, ResourceAction action) { + private ResourceAction addAction(String parentId, ResourceAction action) { RealmModel realm = getRealm(); - ComponentModel policyModel = realm.getComponent(policy.getId()); + 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 @@ -151,14 +169,37 @@ public class ResourcePolicyManager { .map(ResourcePolicy::new).toList(); } - public List getActions(ResourcePolicy policy) { - RealmModel realm = getRealm(); - return realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()) - .map(ResourceAction::new).sorted().toList(); + public List getActions(String policyId) { + return getActionsStream(policyId).toList(); + } + + public Stream 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) { - return getActions(policy).get(0); + return getActions(policy.getId()).get(0); } private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) { @@ -167,10 +208,11 @@ public class ResourcePolicyManager { return (ResourcePolicyProvider) factory.create(session, getRealm().getComponent(policy.getId())); } - private ResourceActionProvider getActionProvider(ResourceAction action) { + public ResourceActionProvider getActionProvider(ResourceAction action) { + RealmModel realm = session.getContext().getRealm(); ComponentFactory actionFactory = (ComponentFactory) session.getKeycloakSessionFactory() .getProviderFactory(ResourceActionProvider.class, action.getProviderId()); - return (ResourceActionProvider) actionFactory.create(session, getRealm().getComponent(action.getId())); + return (ResourceActionProvider) actionFactory.create(session, realm.getComponent(action.getId())); } private RealmModel getRealm() { @@ -238,7 +280,7 @@ public class ResourcePolicyManager { // 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).isEmpty()) + .filter(policy -> policy.isEnabled() && !getActions(policy.getId()).isEmpty()) .forEach(policy -> { ResourcePolicyProvider provider = getPolicyProvider(policy); if (!currentlyAssignedPolicies.contains(policy.getId())) { @@ -254,7 +296,7 @@ public class ResourcePolicyManager { 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).forEach(action -> getActionProvider(action).run(List.of(event.getResourceId()))); + getActions(policy.getId()).forEach(action -> getActionProvider(action).run(List.of(event.getResourceId()))); }); } } @@ -272,7 +314,7 @@ public class ResourcePolicyManager { this.getPolicies().stream().filter(ResourcePolicy::isEnabled).forEach(policy -> { for (ScheduledAction scheduled : policyStateProvider.getDueScheduledActions(policy)) { - List actions = getActions(policy); + List actions = getActions(policy.getId()); for (int i = 0; i < actions.size(); i++) { ResourceAction currentAction = actions.get(i); @@ -329,4 +371,54 @@ public class ResourcePolicyManager { 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 actions = action.getActions().stream().map(this::toRepresentation).toList(); + return new ResourcePolicyActionRepresentation(action.getId(), action.getProviderId(), action.getConfig(), actions); + } + + public ResourcePolicy toModel(ResourcePolicyRepresentation rep) { + ResourcePolicyManager manager = new ResourcePolicyManager(session); + MultivaluedHashMap config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); + + for (ResourcePolicyConditionRepresentation condition : rep.getConditions()) { + String conditionProviderId = condition.getProviderId(); + config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId); + + for (Entry> configEntry : condition.getConfig().entrySet()) { + config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue()); + } + } + + ResourcePolicy policy = manager.addPolicy(rep.getProviderId(), config); + List actions = new ArrayList<>(); + + for (ResourcePolicyActionRepresentation actionRep : rep.getActions()) { + actions.add(toModel(actionRep)); + } + + manager.updateActions(policy, actions); + + return policy; + } + + private ResourceAction toModel(ResourcePolicyActionRepresentation rep) { + List subActions = new ArrayList<>(); + + for (ResourcePolicyActionRepresentation subAction : ofNullable(rep.getActions()).orElse(List.of())) { + subActions.add(toModel(subAction)); + } + + return new ResourceAction(rep.getProviderId(), rep.getConfig(), subActions); + } } diff --git a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java index 0e73c0d677f..669bbb1f299 100644 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java +++ b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java @@ -1,9 +1,6 @@ package org.keycloak.realm.resources.policies.admin.resource; -import java.util.ArrayList; import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -14,13 +11,9 @@ 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.util.MultivaluedHashMap; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.policy.ResourceAction; import org.keycloak.models.policy.ResourcePolicy; import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation; import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; class RealmResourcePoliciesResource { @@ -36,7 +29,7 @@ class RealmResourcePoliciesResource { @POST @Consumes(MediaType.APPLICATION_JSON) public Response create(ResourcePolicyRepresentation rep) { - ResourcePolicy policy = createPolicy(rep); + ResourcePolicy policy = manager.toModel(rep); return Response.created(session.getContext().getUri().getRequestUriBuilder().path(policy.getId()).build()).build(); } @@ -44,7 +37,7 @@ class RealmResourcePoliciesResource { @Consumes(MediaType.APPLICATION_JSON) public Response createAll(List reps) { for (ResourcePolicyRepresentation policy : reps) { - createPolicy(policy); + manager.toModel(policy); } return Response.created(session.getContext().getUri().getRequestUri()).build(); } @@ -63,41 +56,6 @@ class RealmResourcePoliciesResource { @GET @Produces(MediaType.APPLICATION_JSON) public List list() { - return manager.getPolicies().stream().map(this::toRepresentation).toList(); - } - - private ResourcePolicy createPolicy(ResourcePolicyRepresentation rep) { - ResourcePolicyManager manager = new ResourcePolicyManager(session); - MultivaluedHashMap config = Optional.ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); - - for (ResourcePolicyConditionRepresentation condition : rep.getConditions()) { - String conditionProviderId = condition.getProviderId(); - config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId); - - for (Entry> configEntry : condition.getConfig().entrySet()) { - config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue()); - } - } - - ResourcePolicy policy = manager.addPolicy(rep.getProviderId(), config); - List actions = new ArrayList<>(); - - for (ResourcePolicyActionRepresentation actionRep : rep.getActions()) { - actions.add(new ResourceAction(actionRep.getProviderId(), new MultivaluedHashMap<>(actionRep.getConfig()))); - } - - manager.updateActions(policy, actions); - - return policy; - } - - ResourcePolicyRepresentation toRepresentation(ResourcePolicy policy) { - ResourcePolicyRepresentation rep = new ResourcePolicyRepresentation(policy.getId(), policy.getProviderId(), policy.getConfig()); - - for (ResourceAction action : manager.getActions(policy)) { - rep.addAction(new ResourcePolicyActionRepresentation(action.getId(), action.getProviderId(), action.getConfig())); - } - - return rep; + return manager.getPolicies().stream().map(manager::toRepresentation).toList(); } } diff --git a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java index b115268b952..876acabae76 100644 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java +++ b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java @@ -6,10 +6,8 @@ import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Produces; -import org.keycloak.models.policy.ResourceAction; import org.keycloak.models.policy.ResourcePolicy; import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; class RealmResourcePolicyResource { @@ -35,16 +33,6 @@ class RealmResourcePolicyResource { @GET @Produces(APPLICATION_JSON) public ResourcePolicyRepresentation toRepresentation() { - return toRepresentation(policy); - } - - ResourcePolicyRepresentation toRepresentation(ResourcePolicy policy) { - ResourcePolicyRepresentation rep = new ResourcePolicyRepresentation(policy.getId(), policy.getProviderId(), policy.getConfig()); - - for (ResourceAction action : manager.getActions(policy)) { - rep.addAction(new ResourcePolicyActionRepresentation(action.getId(), action.getProviderId(), action.getConfig())); - } - - return rep; + return manager.toRepresentation(policy); } } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory index d9c0cc57377..e863f3b69ca 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory @@ -19,4 +19,5 @@ 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 diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java new file mode 100644 index 00000000000..27ca38b5c4e --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java @@ -0,0 +1,150 @@ +package org.keycloak.tests.admin.model.policy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +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 java.time.Duration; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +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.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.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.remote.runonserver.InjectRunOnServer; +import org.keycloak.testframework.remote.runonserver.RunOnServerClient; + +@KeycloakIntegrationTest(config = RLMServerConfig.class) +public class AggregatedActionTest { + + private static final String REALM_NAME = "default"; + + @InjectRunOnServer(permittedPackages = "org.keycloak.tests") + RunOnServerClient runOnServer; + + @InjectRealm(lifecycle = LifeCycle.METHOD) + ManagedRealm managedRealm; + + @Test + public void testCreate() { + managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() + .of(UserCreationTimeResourcePolicyProviderFactory.ID) + .withActions( + ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID) + .after(Duration.ofDays(5)) + .withActions(ResourcePolicyActionRepresentation.create() + .of(SetUserAttributeActionProviderFactory.ID) + .withConfig("message", "message") + .build(), + ResourcePolicyActionRepresentation.create() + .of(DisableUserActionProviderFactory.ID) + .build() + ).build()) + .build()).close(); + + List 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 actions = aggregatedAction.getActions(); + assertThat(actions, hasSize(2)); + assertAction(actions, SetUserAttributeActionProviderFactory.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 -> { + assertNotNull(a.getConfig()); + assertThat(a.getConfig().isEmpty(), is(false)); + assertThat(a.getConfig(), hasEntry("priority", List.of("2"))); + }); + } + + @Test + public void testActionRun() { + managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() + .of(UserCreationTimeResourcePolicyProviderFactory.ID) + .withActions( + ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID) + .after(Duration.ofDays(5)) + .withActions(ResourcePolicyActionRepresentation.create() + .of(SetUserAttributeActionProviderFactory.ID) + .withConfig("message", "message") + .build(), + ResourcePolicyActionRepresentation.create() + .of(DisableUserActionProviderFactory.ID) + .build() + ).build()) + .build()).close(); + + managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org")).close(); + + runOnServer.run((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, "alice"); + assertNotNull(user.getAttributes().get("message")); + assertFalse(user.isEnabled()); + } finally { + Time.setOffset(0); + } + })); + } + + private static RealmModel configureSessionContext(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(REALM_NAME); + session.getContext().setRealm(realm); + return realm; + } + + private UserRepresentation getUserRepresentation(String username, String firstName, String lastName, String email) { + UserRepresentation representation = new UserRepresentation(); + representation.setUsername(username); + representation.setFirstName(firstName); + representation.setLastName(lastName); + representation.setEmail(email); + representation.setEnabled(true); + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + representation.setCredentials(List.of(credential)); + return representation; + } + + private void assertAction(List actions, String expectedProviderId, Consumer assertions) { + assertTrue(actions.stream() + .anyMatch(a -> { + if (a.getProviderId().equals(expectedProviderId)) { + assertions.accept(a); + return true; + } + return false; + })); + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java index ccc2c937883..57e5073ec8a 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java @@ -233,8 +233,8 @@ public class ResourcePolicyManagementTest { assertEquals(1, registeredPolicies.size()); ResourcePolicy policy = registeredPolicies.get(0); - assertEquals(2, manager.getActions(policy).size()); - ResourceAction notifyAction = manager.getActions(policy).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()); @@ -249,7 +249,7 @@ public class ResourcePolicyManagementTest { user = session.users().getUserById(realm, user.getId()); // Verify that the next action was scheduled for the user - ResourceAction disableAction = manager.getActions(policy).get(1); + 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"); @@ -313,8 +313,8 @@ public class ResourcePolicyManagementTest { assertEquals(1, registeredPolicies.size()); ResourcePolicy policy = registeredPolicies.get(0); - assertEquals(2, policyManager.getActions(policy).size()); - ResourceAction notifyAction = policyManager.getActions(policy).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); @@ -333,7 +333,7 @@ public class ResourcePolicyManagementTest { policyManager.runScheduledActions(); // check the same users are now scheduled to run the second action. - ResourceAction disableAction = policyManager.getActions(policy).get(1); + ResourceAction disableAction = policyManager.getActions(policy.getId()).get(1); scheduledActions = stateProvider.getScheduledActionsByPolicy(policy); assertEquals(3, scheduledActions.size()); scheduledActions.forEach(scheduledAction -> { @@ -516,7 +516,7 @@ public class ResourcePolicyManagementTest { UserModel user = session.users().getUserByUsername(realm, "testuser"); ResourcePolicy policy = manager.getPolicies().get(0); - ResourceAction action = manager.getActions(policy).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);