Initial refactoring to make federated identities a condition

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-09-01 15:38:19 -03:00
parent 17a053b2af
commit 03cbc11e7e
12 changed files with 225 additions and 30 deletions

View File

@ -26,15 +26,22 @@ import jakarta.persistence.criteria.Subquery;
import java.util.ArrayList;
import java.util.Collections;
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.connections.jpa.JpaConnectionProvider;
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.jpa.entities.FederatedIdentityEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionProvider;
public abstract class AbstractUserResourcePolicyProvider implements ResourcePolicyProvider {
@ -42,8 +49,6 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
private final EntityManager em;
private final KeycloakSession session;
private static final String BROKER_ALIASES = "broker-aliases";
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
this.policyModel = model;
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
@ -187,6 +192,41 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
}
protected List<String> getBrokerAliases() {
return getModel().getConfig().getOrDefault(BROKER_ALIASES, List.of());
List<String> conditions = policyModel.getConfig().getOrDefault("conditions", List.of());
for (String providerId : conditions) {
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
if (condition instanceof IdentityProviderPolicyConditionProvider) {
return getModel().getConfig().getOrDefault(providerId + "." + IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, List.of());
}
}
return List.of();
}
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 : policyModel.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

@ -30,7 +30,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
}
@Override
public boolean scheduleOnEvent(ResourcePolicyEvent event) {
public boolean activateOnEvent(ResourcePolicyEvent event) {
List<String> events = model.getConfig().getOrDefault("events", List.of());
ResourceOperationType operation = event.getOperation();
@ -51,6 +51,11 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
return true;
}
@Override
public boolean deactivateOnEvent(ResourcePolicyEvent event) {
return false;
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
return false;
@ -82,6 +87,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
if (condition == null) {
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
}
return condition;
}
}

View File

@ -1,10 +1,21 @@
package org.keycloak.models.policy;
/*
* 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.conditions;
import java.util.List;
import java.util.Map;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
public class GroupMembershipPolicyConditionFactory implements ResourcePolicyConditionProviderFactory<GroupMembershipPolicyConditionProvider> {

View File

@ -1,4 +1,14 @@
package org.keycloak.models.policy;
/*
* 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.conditions;
import java.util.List;
@ -6,8 +16,11 @@ import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
import org.keycloak.models.policy.ResourcePolicyEvent;
import org.keycloak.models.policy.ResourceType;
public class GroupMembershipPolicyConditionProvider implements ResourcePolicyConditionProvider {
public class GroupMembershipPolicyConditionProvider implements ResourcePolicyConditionProvider {
private final List<String> expectedGroups;
private final KeycloakSession session;

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.conditions;
import java.util.List;
import java.util.Map;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
public class IdentityProviderPolicyConditionFactory implements ResourcePolicyConditionProviderFactory<IdentityProviderPolicyConditionProvider> {
public static final String ID = "identity-provider-condition";
public static final String EXPECTED_ALIASES = "alias";
@Override
public IdentityProviderPolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
return new IdentityProviderPolicyConditionProvider(session, config.get(EXPECTED_ALIASES));
}
@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,55 @@
/*
* 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.conditions;
import java.util.List;
import java.util.stream.Stream;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.policy.ResourcePolicyConditionProvider;
import org.keycloak.models.policy.ResourcePolicyEvent;
import org.keycloak.models.policy.ResourceType;
public class IdentityProviderPolicyConditionProvider implements ResourcePolicyConditionProvider {
private final List<String> expectedAliases;
private final KeycloakSession session;
public IdentityProviderPolicyConditionProvider(KeycloakSession session, List<String> expectedAliases) {
this.session = session;
this.expectedAliases = expectedAliases;;
}
@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);
Stream<FederatedIdentityModel> federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user);
return federatedIdentities
.map(FederatedIdentityModel::getIdentityProvider)
.anyMatch(expectedAliases::contains);
}
@Override
public void close() {
}
}

View File

@ -15,4 +15,5 @@
# limitations under the License.
#
org.keycloak.models.policy.GroupMembershipPolicyConditionFactory
org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory
org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory

View File

@ -1,9 +1,13 @@
package org.keycloak.models.policy;
import java.util.List;
import java.util.Map;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.FederatedIdentityModel.FederatedIdentityCreatedEvent;
import org.keycloak.models.FederatedIdentityModel.FederatedIdentityRemovedEvent;
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
import org.keycloak.provider.ProviderEvent;
@ -11,8 +15,8 @@ public enum ResourceOperationType {
CREATE(OperationType.CREATE, EventType.REGISTER),
LOGIN(EventType.LOGIN),
ADD_FEDERATED_IDENTITY,
REMOVE_FEDERATED_IDENTITY,
ADD_FEDERATED_IDENTITY(FederatedIdentityCreatedEvent.class),
REMOVE_FEDERATED_IDENTITY(FederatedIdentityRemovedEvent.class),
GROUP_MEMBERSHIP_JOIN(GroupMemberJoinEvent.class);
private final List<Object> types;
@ -41,6 +45,20 @@ public enum ResourceOperationType {
}
for (Object type : value.types) {
if (type instanceof Class<?> cls && cls.isAssignableFrom((Class<?>) from)) {
// 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);
// }
// });
return value;
}
}
@ -52,6 +70,12 @@ public enum ResourceOperationType {
if (event instanceof GroupMemberJoinEvent gme) {
return gme.getUser().getId();
}
if (event instanceof FederatedIdentityModel.FederatedIdentityCreatedEvent fie) {
return fie.getUser().getId();
}
if (event instanceof FederatedIdentityModel.FederatedIdentityRemovedEvent fie) {
return fie.getUser().getId();
}
return null;
}
}

View File

@ -1,26 +1,15 @@
package org.keycloak.models.policy;
import java.util.HashMap;
import java.util.Map;
public class ResourcePolicyEvent {
private final ResourceType type;
private final ResourceOperationType operation;
private final String resourceId;
private final Map<String, String> details = new HashMap<>();
public ResourcePolicyEvent(ResourceType type, ResourceOperationType operation, String resourceId) {
this(type, operation, resourceId, null);
}
public ResourcePolicyEvent(ResourceType type, ResourceOperationType operation, String resourceId, Map<String, String> details) {
this.type = type;
this.operation = operation;
this.resourceId = resourceId;
if (details != null) {
this.details.putAll(details);
}
}
public ResourceType getResourceType() {
@ -34,8 +23,4 @@ public class ResourcePolicyEvent {
public String getResourceId() {
return resourceId;
}
public Map<String, String> getDetails() {
return details;
}
}

View File

@ -38,10 +38,12 @@ import org.keycloak.models.policy.ResourcePolicy;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.ResourcePolicyStateProvider;
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectRealm;
@ -117,7 +119,10 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
public void tesRunActionOnFederatedUser() {
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withConfig("broker-aliases", IDP_OIDC_ALIAS)
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(IdentityProviderPolicyConditionFactory.ID)
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
.build())
.withActions(
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
.after(Duration.ofDays(1))
@ -184,7 +189,10 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
public void testAddRemoveFedIdentityAffectsPolicyAssociation() {
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withConfig("broker-aliases", IDP_OIDC_ALIAS)
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(IdentityProviderPolicyConditionFactory.ID)
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
.build())
.withActions(
ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID)
.after(Duration.ofDays(1))

View File

@ -34,7 +34,7 @@ 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.conditions.GroupMembershipPolicyConditionFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourceOperationType;
import org.keycloak.models.policy.ResourcePolicyManager;
@ -110,7 +110,7 @@ public class GroupMembershipJoinPolicyTest {
try {
// set offset to 7 days - notify action should run now
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
manager.runScheduledTasks();
manager.runScheduledActions();
user = session.users().getUserById(realm, userId);
assertNotNull(user.getAttributes().get("message"));
} finally {

View File

@ -45,7 +45,9 @@ import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.ResourcePolicyStateProvider;
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectUser;
@ -258,7 +260,10 @@ public class ResourcePolicyManagementTest {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.withConfig("broker-aliases", "someidp")
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(IdentityProviderPolicyConditionFactory.ID)
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, "someidp")
.build())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))