mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
Add toPredicate implementation for conditions
Closes #42696 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowExecutionContext;
|
||||
@@ -34,6 +41,29 @@ public class GroupMembershipWorkflowConditionProvider implements WorkflowConditi
|
||||
return user.isMemberOf(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> path) {
|
||||
validate();
|
||||
|
||||
GroupModel group = KeycloakModelUtils.findGroupByPath(session, session.getContext().getRealm(), expectedGroup);
|
||||
if (group == null) {
|
||||
return cb.disjunction(); // always false
|
||||
}
|
||||
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<UserGroupMembershipEntity> from = subquery.from(UserGroupMembershipEntity.class);
|
||||
|
||||
subquery.select(cb.literal(1));
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(from.get("user").get("id"), path.get("id")),
|
||||
cb.equal(from.get("groupId"), group.getId())
|
||||
)
|
||||
);
|
||||
|
||||
return cb.exists(subquery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
if (StringUtil.isBlank(this.expectedGroup)) {
|
||||
|
||||
@@ -3,11 +3,18 @@ package org.keycloak.models.workflow.conditions;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowExecutionContext;
|
||||
@@ -40,6 +47,29 @@ public class RoleWorkflowConditionProvider implements WorkflowConditionProvider
|
||||
return role != null && RoleUtils.hasRole(roles, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> path) {
|
||||
validate();
|
||||
|
||||
RoleModel role = getRole(expectedRole, session.getContext().getRealm());
|
||||
if (role == null) {
|
||||
return cb.disjunction(); // always false
|
||||
}
|
||||
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<UserRoleMappingEntity> from = subquery.from(UserRoleMappingEntity.class);
|
||||
|
||||
subquery.select(cb.literal(1));
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(from.get("user").get("id"), path.get("id")),
|
||||
cb.equal(from.get("roleId"), role.getId())
|
||||
)
|
||||
);
|
||||
|
||||
return cb.exists(subquery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws WorkflowInvalidStateException {
|
||||
if (StringUtil.isBlank(expectedRole)) {
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package org.keycloak.models.workflow.conditions;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.UserAttributeEntity;
|
||||
import org.keycloak.models.workflow.WorkflowConditionProvider;
|
||||
import org.keycloak.models.workflow.WorkflowExecutionContext;
|
||||
import org.keycloak.models.workflow.WorkflowInvalidStateException;
|
||||
import org.keycloak.storage.jpa.JpaHashUtils;
|
||||
|
||||
import static org.keycloak.common.util.CollectionUtil.collectionEquals;
|
||||
|
||||
@@ -41,6 +50,63 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition
|
||||
return collectionEquals(expectedValues, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> path) {
|
||||
validate();
|
||||
|
||||
String[] parsedKeyValuePair = parseKeyValuePair(expectedAttribute);
|
||||
String attributeName = parsedKeyValuePair[0];
|
||||
List<String> expectedValues = Arrays.asList(parsedKeyValuePair[1].split(","));
|
||||
|
||||
// Subquery to count how many of the expected values the user has
|
||||
// to check if there is no missing value
|
||||
Subquery<Long> matchingCountSubquery = query.subquery(Long.class);
|
||||
Root<UserAttributeEntity> attrRoot1 = matchingCountSubquery.from(UserAttributeEntity.class);
|
||||
matchingCountSubquery.select(cb.count(attrRoot1));
|
||||
|
||||
// Build predicate for matching values
|
||||
// For values <= 255 chars: compare against 'value' field
|
||||
// For values > 255 chars: compare against 'longValueHash' field (to avoid Oracle NCLOB comparison issues)
|
||||
Predicate[] valuePredicates = expectedValues.stream()
|
||||
.map(expectedValue -> {
|
||||
if (expectedValue.length() > 255) {
|
||||
// Use hash comparison for long values to avoid NCLOB comparison issues in Oracle
|
||||
return cb.equal(attrRoot1.get("longValueHash"), JpaHashUtils.hashForAttributeValue(expectedValue));
|
||||
} else {
|
||||
// For short values, compare directly
|
||||
return cb.equal(attrRoot1.get("value"), expectedValue);
|
||||
}
|
||||
})
|
||||
.toArray(Predicate[]::new);
|
||||
|
||||
matchingCountSubquery.where(
|
||||
cb.and(
|
||||
cb.equal(attrRoot1.get("user").get("id"), path.get("id")),
|
||||
cb.equal(attrRoot1.get("name"), attributeName),
|
||||
cb.or(valuePredicates)
|
||||
)
|
||||
);
|
||||
|
||||
// Subquery to count total attributes with this name for the user
|
||||
// to check if there are no extra values
|
||||
Subquery<Long> totalCountSubquery = query.subquery(Long.class);
|
||||
Root<UserAttributeEntity> attrRoot2 = totalCountSubquery.from(UserAttributeEntity.class);
|
||||
totalCountSubquery.select(cb.count(attrRoot2));
|
||||
totalCountSubquery.where(
|
||||
cb.and(
|
||||
cb.equal(attrRoot2.get("user").get("id"), path.get("id")),
|
||||
cb.equal(attrRoot2.get("name"), attributeName)
|
||||
)
|
||||
);
|
||||
|
||||
// Both counts must equal the expected count (exact match)
|
||||
int expectedCount = expectedValues.size();
|
||||
return cb.and(
|
||||
cb.equal(matchingCountSubquery, expectedCount),
|
||||
cb.equal(totalCountSubquery, expectedCount)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
if (expectedAttribute == null) {
|
||||
|
||||
@@ -11,9 +11,7 @@ public interface WorkflowConditionProvider extends Provider {
|
||||
|
||||
boolean evaluate(WorkflowExecutionContext context);
|
||||
|
||||
default Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> resourceRoot) {
|
||||
return null;
|
||||
}
|
||||
Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<?> resourceRoot);
|
||||
|
||||
void validate() throws WorkflowInvalidStateException;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import jakarta.ws.rs.core.Response.Status;
|
||||
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowProvider;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
@@ -21,6 +25,7 @@ import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.GroupConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
@@ -135,4 +140,49 @@ public class GroupMembershipJoinWorkflowTest extends AbstractWorkflowTest {
|
||||
assertThat(status.getErrors().get(0), containsString("Group with name %s does not exist.".formatted("generic-group")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActivateWorkflowForEligibleResources() {
|
||||
managedRealm.admin().groups().add(GroupConfigBuilder.create().name("groupA").build()).close();
|
||||
|
||||
// create some users associated with a group membership
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("group-member-" + i)
|
||||
.groups("groupA").build()).close();
|
||||
}
|
||||
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.withName("groupA-membership-workflow")
|
||||
.onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_ADDED.name())
|
||||
.onCondition(GroupMembershipWorkflowConditionFactory.ID + "(groupA)")
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
// activate the workflow for all eligible users
|
||||
provider.activateForAllEligibleResources(registeredWorkflows.get(0));
|
||||
});
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
// check workflow was correctly assigned to the users
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertThat(scheduledSteps, hasSize(10));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.RestartWorkflowStepProviderFactory;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowProvider;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
@@ -23,12 +28,14 @@ import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.RoleConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
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;
|
||||
@@ -62,6 +69,54 @@ public class RoleWorkflowConditionTest extends AbstractWorkflowTest {
|
||||
assertUserRoles("user-3", true, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActivateWorkflowForEligibleResources() {
|
||||
RoleRepresentation role = createRoleIfNotExists("testRole");
|
||||
|
||||
// create some users associated with the role
|
||||
for (int i = 0; i < 10; i++) {
|
||||
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-with-role-" + i).build())) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
managedRealm.admin().users().get(ApiUtil.getCreatedId(response)).roles().realmLevel().add(List.of(role));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.withName("test-role-workflow")
|
||||
.onEvent(ResourceOperationType.USER_ROLE_ADDED.name())
|
||||
.onCondition(RoleWorkflowConditionFactory.ID + "(testRole)")
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build()).close();
|
||||
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
// activate the workflow for all eligible users
|
||||
provider.activateForAllEligibleResources(registeredWorkflows.get(0));
|
||||
});
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
// check workflow was correctly assigned to the users
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertThat(scheduledSteps, hasSize(10));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertUserRoles(String username, boolean shouldExist, String... roles) {
|
||||
assertUserRoles(username, shouldExist, List.of(roles));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.models.workflow.RestartWorkflowStepProviderFactory;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowProvider;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
@@ -21,11 +24,13 @@ import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
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;
|
||||
@@ -71,6 +76,41 @@ public class UserAttributeWorkflowConditionTest extends AbstractWorkflowTest {
|
||||
assertUserAttribute("user-4", true, values);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActivateWorkflowForEligibleResources() {
|
||||
// create some users with attributes
|
||||
for (int i = 0; i < 10; i++) {
|
||||
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-with-attr-" + i)
|
||||
.attribute("key", "value").build())) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
createWorkflow(Map.of("key", List.of("value")));
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
// activate the workflow for all eligible users
|
||||
provider.activateForAllEligibleResources(registeredWorkflows.get(0));
|
||||
});
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
||||
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
|
||||
assertThat(registeredWorkflows, hasSize(1));
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
// check workflow was correctly assigned to the users
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertThat(scheduledSteps, hasSize(10));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertUserAttribute(String username, boolean shouldExist, String... values) {
|
||||
assertUserAttribute(username, shouldExist, Map.of("attribute", List.of(values)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user