Add support for generic event-based policies and conditions

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-08-29 18:20:25 -03:00
parent 7990fa0300
commit 17a053b2af
24 changed files with 714 additions and 48 deletions

View File

@ -0,0 +1,77 @@
package org.keycloak.representations.resources.policies;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ResourcePolicyConditionRepresentation {
public static Builder create() {
return new Builder();
}
private String id;
private String providerId;
private Map<String, List<String>> config;
public ResourcePolicyConditionRepresentation() {
// reflection
}
public ResourcePolicyConditionRepresentation(String providerId) {
this(providerId, null);
}
public ResourcePolicyConditionRepresentation(String providerId, Map<String, List<String>> config) {
this(null, providerId, config);
}
public ResourcePolicyConditionRepresentation(String id, String providerId, Map<String, List<String>> config) {
this.id = id;
this.providerId = providerId;
this.config = config;
}
public String getProviderId() {
return providerId;
}
public void setProviderId(String providerId) {
this.providerId = providerId;
}
public Map<String, List<String>> getConfig() {
return config;
}
public void setConfig(Map<String, List<String>> config) {
this.config = config;
}
public void setConfig(String key, String value) {
if (this.config == null) {
this.config = new HashMap<>();
}
this.config.put(key, Collections.singletonList(value));
}
public static class Builder {
private ResourcePolicyConditionRepresentation action;
public Builder of(String providerId) {
this.action = new ResourcePolicyConditionRepresentation(providerId);
return this;
}
public Builder withConfig(String key, String value) {
action.setConfig(key, value);
return this;
}
public ResourcePolicyConditionRepresentation build() {
return action;
}
}
}

View File

@ -21,6 +21,7 @@ public class ResourcePolicyRepresentation {
private String providerId;
private MultivaluedHashMap<String, String> config;
private List<ResourcePolicyActionRepresentation> actions;
private List<ResourcePolicyConditionRepresentation> conditions;
public ResourcePolicyRepresentation() {
// reflection
@ -67,6 +68,14 @@ public class ResourcePolicyRepresentation {
this.config.putSingle("name", name);
}
public void setConditions(List<ResourcePolicyConditionRepresentation> conditions) {
this.conditions = conditions;
}
public List<ResourcePolicyConditionRepresentation> getConditions() {
return conditions;
}
public void setActions(List<ResourcePolicyActionRepresentation> actions) {
this.actions = actions;
}
@ -89,6 +98,7 @@ public class ResourcePolicyRepresentation {
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 List<Builder> builders = new ArrayList<>();
@ -106,6 +116,22 @@ public class ResourcePolicyRepresentation {
return builder;
}
public Builder onEvent(String operation) {
List<String> events = config.computeIfAbsent("events", k -> new ArrayList<>());
events.add(operation);
return this;
}
public Builder onCoditions(ResourcePolicyConditionRepresentation... condition) {
if (conditions == null) {
conditions = new ArrayList<>();
}
conditions.addAll(Arrays.asList(condition));
return this;
}
public Builder withActions(ResourcePolicyActionRepresentation... actions) {
this.actions.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(actions));
return this;
@ -133,6 +159,7 @@ public class ResourcePolicyRepresentation {
ResourcePolicyRepresentation policy = new ResourcePolicyRepresentation(entry.getKey(), builder.config);
policy.setActions(entry.getValue());
policy.setConditions(builder.conditions);
policies.add(policy);
}

View File

@ -459,7 +459,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
em.persist(entity);
em.flush();
em.detach(entity);
GroupMemberJoinEvent.fire(group, session);
GroupMemberJoinEvent.fire(group, this, session);
}
@Override

View File

@ -0,0 +1,87 @@
package org.keycloak.models.policy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider {
private final KeycloakSession session;
private final ComponentModel model;
public EventBasedResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
}
@Override
public List<String> getEligibleResourcesForInitialAction() {
return List.of();
}
@Override
public boolean supports(ResourceType type) {
return ResourceType.USERS.equals(type);
}
@Override
public boolean scheduleOnEvent(ResourcePolicyEvent event) {
List<String> events = model.getConfig().getOrDefault("events", List.of());
ResourceOperationType operation = event.getOperation();
if (!events.contains(operation.name())) {
return false;
}
List<String> conditions = model.getConfig().getOrDefault("conditions", List.of());
for (String providerId : conditions) {
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
if (!condition.evaluate(event)) {
return false;
}
}
return true;
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
return false;
}
@Override
public void close() {
}
private ResourcePolicyConditionProvider resolveCondition(String providerId) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider> providerFactory = (ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider>) sessionFactory.getProviderFactory(ResourcePolicyConditionProvider.class, providerId);
if (providerFactory == null) {
throw new IllegalStateException("Could not find condition provider: " + providerId);
}
Map<String, List<String>> config = new HashMap<>();
for (Entry<String, List<String>> configEntry : model.getConfig().entrySet()) {
if (configEntry.getKey().startsWith(providerId)) {
config.put(configEntry.getKey().substring(providerId.length() + 1), configEntry.getValue());
}
}
ResourcePolicyConditionProvider condition = providerFactory.create(session, config);
if (condition == null) {
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
}
return condition;
}
}

View File

@ -0,0 +1,50 @@
package org.keycloak.models.policy;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class EventBasedResourcePolicyProviderFactory implements ResourcePolicyProviderFactory {
public static final String ID = "event-based-resource-policy";
@Override
public ResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new EventBasedResourcePolicyProvider(session, model);
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getHelpText() {
return "";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}
@Override
public String getId() {
return ID;
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,36 @@
package org.keycloak.models.policy;
import java.util.List;
import java.util.Map;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class GroupMembershipPolicyConditionFactory implements ResourcePolicyConditionProviderFactory<GroupMembershipPolicyConditionProvider> {
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));
}
@Override
public String getId() {
return ID;
}
@Override
public void init(org.keycloak.Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,45 @@
package org.keycloak.models.policy;
import java.util.List;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
public class GroupMembershipPolicyConditionProvider implements ResourcePolicyConditionProvider {
private final List<String> expectedGroups;
private final KeycloakSession session;
public GroupMembershipPolicyConditionProvider(KeycloakSession session, List<String> expectedGroups) {
this.session = session;
this.expectedGroups = expectedGroups;;
}
@Override
public boolean evaluate(ResourcePolicyEvent event) {
if (!ResourceType.USERS.equals(event.getResourceType())) {
return false;
}
String userId = event.getResourceId();
RealmModel realm = session.getContext().getRealm();
UserModel user = session.users().getUserById(realm, userId);
for (String expectedGroup : expectedGroups) {
GroupModel group = session.groups().getGroupById(realm, expectedGroup);
if (user.isMemberOf(group)) {
return true;
}
}
return false;
}
@Override
public void close() {
}
}

View File

@ -29,11 +29,6 @@ public class UserCreationTimeResourcePolicyProviderFactory implements ResourcePo
public static final String ID = "user-creation-time-resource-policy";
@Override
public ResourceType getType() {
return ResourceType.USERS;
}
@Override
public UserCreationTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserCreationTimeResourcePolicyProvider(session, model);

View File

@ -29,11 +29,6 @@ public class UserSessionRefreshTimeResourcePolicyProviderFactory implements Reso
public static final String ID = "user-refresh-time-resource-policy";
@Override
public ResourceType getType() {
return ResourceType.USERS;
}
@Override
public UserSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserSessionRefreshTimeResourcePolicyProvider(session, model);

View File

@ -0,0 +1,18 @@
#
# 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.
#
org.keycloak.models.policy.GroupMembershipPolicyConditionFactory

View File

@ -16,4 +16,5 @@
#
org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory
org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory

View File

@ -1,10 +1,57 @@
package org.keycloak.models.policy;
import java.util.List;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
import org.keycloak.provider.ProviderEvent;
public enum ResourceOperationType {
CREATE,
LOGIN,
CREATE(OperationType.CREATE, EventType.REGISTER),
LOGIN(EventType.LOGIN),
ADD_FEDERATED_IDENTITY,
REMOVE_FEDERATED_IDENTITY
REMOVE_FEDERATED_IDENTITY,
GROUP_MEMBERSHIP_JOIN(GroupMemberJoinEvent.class);
private final List<Object> types;
ResourceOperationType(Enum<?>... types) {
this.types = List.of(types);
}
@SafeVarargs
ResourceOperationType(Class<? extends ProviderEvent>... types) {
this.types = List.of(types);
}
public static ResourceOperationType toOperationType(Enum<?> from) {
return toOperationType((Object) from);
}
public static ResourceOperationType toOperationType(Class<?> from) {
return toOperationType((Object) from);
}
private static ResourceOperationType toOperationType(Object from) {
for (ResourceOperationType value : values()) {
if (value.types.contains(from)) {
return value;
}
for (Object type : value.types) {
if (type instanceof Class<?> cls && cls.isAssignableFrom((Class<?>) from)) {
return value;
}
}
}
return null;
}
public String getResourceId(ProviderEvent event) {
if (event instanceof GroupMemberJoinEvent gme) {
return gme.getUser().getId();
}
return null;
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 org.keycloak.provider.Provider;
public interface ResourcePolicyConditionProvider extends Provider {
boolean evaluate(ResourcePolicyEvent event);
}

View File

@ -0,0 +1,42 @@
/*
* 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.util.List;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.common.Profile;
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 {
P create(KeycloakSession session, Map<String, List<String>> config);
@Override
default P create(KeycloakSession session) {
throw new IllegalStateException("Use create(KeycloakSession session, MultivaluedHashMap<String, String> config) instead.");
}
@Override
default boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ResourcePolicyConditionSpi implements Spi {
public static final String NAME = "rlm-policy-condition";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return ResourcePolicyConditionProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ResourcePolicyConditionProviderFactory.class;
}
}

View File

@ -24,8 +24,6 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory;
public interface ResourcePolicyProviderFactory<P extends ResourcePolicyProvider> extends ComponentFactory<P, ResourcePolicyProvider>, EnvironmentDependentProviderFactory {
ResourceType getType();
@Override
default boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);

View File

@ -17,10 +17,13 @@
package org.keycloak.models.policy;
import static org.keycloak.models.policy.ResourceOperationType.toOperationType;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.OperationType;
import org.keycloak.provider.ProviderEvent;
import java.util.List;
import java.util.Objects;
@ -66,19 +69,19 @@ public enum ResourceType {
return null;
}
private ResourceOperationType toOperationType(OperationType operation) {
return switch (operation) {
case CREATE -> ResourceOperationType.CREATE;
default -> null;
};
}
public ResourcePolicyEvent toEvent(ProviderEvent event) {
ResourceOperationType resourceOperationType = toOperationType(event.getClass());
private ResourceOperationType toOperationType(EventType type) {
return switch (type) {
case REGISTER -> ResourceOperationType.CREATE;
case LOGIN -> ResourceOperationType.LOGIN;
default -> null;
};
}
if (resourceOperationType == null) {
return null;
}
String resourceId = resourceOperationType.getResourceId(event);
if (resourceId == null) {
return null;
}
return new ResourcePolicyEvent(this, resourceOperationType, resourceId);
}
}

View File

@ -107,3 +107,4 @@ org.keycloak.securityprofile.SecurityProfileSpi
org.keycloak.logging.MappedDiagnosticContextSpi
org.keycloak.models.policy.ResourceActionSpi
org.keycloak.models.policy.ResourcePolicySpi
org.keycloak.models.policy.ResourcePolicyConditionSpi

View File

@ -110,7 +110,7 @@ public interface GroupModel extends RoleMapperModel {
}
interface GroupMemberJoinEvent extends GroupEvent {
static void fire(GroupModel group, KeycloakSession session) {
static void fire(GroupModel group, UserModel user, KeycloakSession session) {
session.getKeycloakSessionFactory().publish(new GroupMemberJoinEvent() {
@Override
public RealmModel getRealm() {
@ -122,12 +122,19 @@ public interface GroupModel extends RoleMapperModel {
return group;
}
@Override
public UserModel getUser() {
return user;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
}
UserModel getUser();
}
interface GroupMemberLeaveEvent extends GroupEvent {

View File

@ -17,9 +17,15 @@
package org.keycloak.provider;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ProviderEvent {
default KeycloakSession getKeycloakSession() {
return null;
}
}

View File

@ -14,8 +14,11 @@ import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
public class ResourcePolicyEventListener implements EventListenerProvider {
public class ResourcePolicyEventListener implements EventListenerProvider, ProviderEventListener {
private final KeycloakSession session;
@ -35,6 +38,17 @@ public class ResourcePolicyEventListener implements EventListenerProvider {
trySchedule(policyEvent);
}
@Override
public void onEvent(ProviderEvent event) {
RealmModel realm = session.getContext().getRealm();
if (realm == null) {
return;
}
trySchedule(ResourceType.USERS.toEvent(event));
}
private void trySchedule(ResourcePolicyEvent event) {
if (event != null) {
ResourcePolicyManager manager = new ResourcePolicyManager(session);

View File

@ -21,12 +21,11 @@ import org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderEvent;
import java.util.Map;
@ -48,22 +47,27 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider
@Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(fired -> {
ResourcePolicyEvent rpe = null;
if (fired instanceof FederatedIdentityModel.FederatedIdentityCreatedEvent event) {
rpe = new ResourcePolicyEvent(ResourceType.USERS, ResourceOperationType.ADD_FEDERATED_IDENTITY,
event.getUser().getId(), Map.of("provider", event.getFederatedIdentity().getIdentityProvider()));
ResourcePolicyManager manager = new ResourcePolicyManager(event.getKeycloakSession());
manager.processEvent(rpe);
} else if (fired instanceof FederatedIdentityModel.FederatedIdentityRemovedEvent event) {
rpe = new ResourcePolicyEvent(ResourceType.USERS, ResourceOperationType.REMOVE_FEDERATED_IDENTITY,
event.getUser().getId(), Map.of("provider", event.getFederatedIdentity().getIdentityProvider()));
ResourcePolicyManager manager = new ResourcePolicyManager(event.getKeycloakSession());
manager.processEvent(rpe);
factory.register(event -> {
KeycloakSession session = event.getKeycloakSession();
if (session != null) {
// try first running within the session/transaction the event was fired
onEvent(event, session);
return;
}
// fallback to running in a new session/transaction
KeycloakModelUtils.runJobInTransaction(factory, s -> {
onEvent(event, s);
});
});
}
private void onEvent(ProviderEvent event, KeycloakSession session) {
ResourcePolicyEventListener provider = (ResourcePolicyEventListener) session.getProvider(EventListenerProvider.class, getId());
provider.onEvent(event);
}
@Override
public void close() {
}

View File

@ -2,6 +2,8 @@ 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;
@ -18,6 +20,7 @@ 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 {
@ -65,7 +68,18 @@ class RealmResourcePoliciesResource {
private ResourcePolicy createPolicy(ResourcePolicyRepresentation rep) {
ResourcePolicyManager manager = new ResourcePolicyManager(session);
ResourcePolicy policy = manager.addPolicy(rep.getProviderId(), rep.getConfig());
MultivaluedHashMap<String, String> 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<String, List<String>> configEntry : condition.getConfig().entrySet()) {
config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue());
}
}
ResourcePolicy policy = manager.addPolicy(rep.getProviderId(), config);
List<ResourceAction> actions = new ArrayList<>();
for (ResourcePolicyActionRepresentation actionRep : rep.getActions()) {

View File

@ -0,0 +1,127 @@
/*
* 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.time.Duration;
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.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.GroupMembershipPolicyConditionFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourceOperationType;
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;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.GroupConfigBuilder;
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 org.keycloak.testframework.util.ApiUtil;
@KeycloakIntegrationTest(config = RLMServerConfig.class)
public class GroupMembershipJoinPolicyTest {
private static final String REALM_NAME = "default";
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
RunOnServerClient runOnServer;
@InjectRealm(lifecycle = LifeCycle.METHOD)
ManagedRealm managedRealm;
@Test
public void testEventsOnGroupMembershipJoin() {
String groupId;
try (Response response = managedRealm.admin().groups().add(GroupConfigBuilder.create()
.name("generic-group").build())) {
groupId = ApiUtil.getCreatedId(response);
}
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
.of(EventBasedResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.GROUP_MEMBERSHIP_JOIN.name())
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(GroupMembershipPolicyConditionFactory.ID)
.withConfig(GroupMembershipPolicyConditionFactory.EXPECTED_GROUPS, groupId)
.build())
.withActions(
ResourcePolicyActionRepresentation.create()
.of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build();
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
try (Response response = policies.create(expectedPolicies)) {
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
}
String userId;
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create()
.username("generic-user").build())) {
userId = ApiUtil.getCreatedId(response);
}
managedRealm.admin().users().get(userId).joinGroup(groupId);
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
UserModel user = session.users().getUserById(realm, userId);
assertNull(user.getAttributes().get("message"));
try {
// set offset to 7 days - notify action should run now
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
manager.runScheduledTasks();
user = session.users().getUserById(realm, userId);
assertNotNull(user.getAttributes().get("message"));
} finally {
Time.setOffset(0);
}
}));
}
private static RealmModel configureSessionContext(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
session.getContext().setRealm(realm);
return realm;
}
}