mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Add support for generic event-based policies and conditions
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
7990fa0300
commit
17a053b2af
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
@ -16,4 +16,5 @@
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user