diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java index a6e7fd6a261..41d0303adbb 100644 --- a/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java @@ -50,8 +50,17 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition } String[] parsedKeyValuePair = parseKeyValuePair(expectedAttribute); - List values = user.getAttributes().getOrDefault(parsedKeyValuePair[0], List.of()); - List expectedValues = List.of(parsedKeyValuePair[1].split(",")); + String key = parsedKeyValuePair[0]; + String valuePart = parsedKeyValuePair[1]; + + // Presence-only: "key:" -> true if user has at least one attribute with that key + if (valuePart.isEmpty()) { + List values = user.getAttributes().getOrDefault(key, List.of()); + return !values.isEmpty(); + } + + List values = user.getAttributes().getOrDefault(key, List.of()); + List expectedValues = List.of(valuePart.split(",")); return collectionEquals(expectedValues, values); } @@ -62,7 +71,14 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition String[] parsedKeyValuePair = parseKeyValuePair(expectedAttribute); String attributeName = parsedKeyValuePair[0]; - List expectedValues = Arrays.asList(parsedKeyValuePair[1].split(",")); + String valuePart = parsedKeyValuePair[1]; + + // Presence-only: require at least one attribute with this name for the user + if (valuePart.isEmpty()) { + return cb.greaterThan(createTotalCountSubquery(cb, query, path, attributeName), 0L); + } + + List expectedValues = Arrays.asList(valuePart.split(",")); // Subquery to count how many of the expected values the user has // to check if there is no missing value @@ -95,24 +111,29 @@ public class UserAttributeWorkflowConditionProvider implements WorkflowCondition // Subquery to count total attributes with this name for the user // to check if there are no extra values - Subquery totalCountSubquery = query.subquery(Long.class); - Root 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) - ) - ); + createTotalCountSubquery(cb, query, path, 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) + cb.equal(createTotalCountSubquery(cb, query, path, attributeName), expectedCount) ); } + private Subquery createTotalCountSubquery(CriteriaBuilder cb, CriteriaQuery query, Root path, String attributeName) { + Subquery totalCountSubquery = query.subquery(Long.class); + Root attrRoot = totalCountSubquery.from(UserAttributeEntity.class); + totalCountSubquery.select(cb.count(attrRoot)); + totalCountSubquery.where( + cb.and( + cb.equal(attrRoot.get("user").get("id"), path.get("id")), + cb.equal(attrRoot.get("name"), attributeName) + ) + ); + return totalCountSubquery; + } + @Override public void validate() { if (expectedAttribute == null) { diff --git a/tests/base/src/test/java/org/keycloak/tests/workflow/condition/UserAttributeWorkflowConditionTest.java b/tests/base/src/test/java/org/keycloak/tests/workflow/condition/UserAttributeWorkflowConditionTest.java index b33fee69868..7ecae0ff083 100644 --- a/tests/base/src/test/java/org/keycloak/tests/workflow/condition/UserAttributeWorkflowConditionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/workflow/condition/UserAttributeWorkflowConditionTest.java @@ -51,6 +51,14 @@ public class UserAttributeWorkflowConditionTest extends AbstractWorkflowTest { managedRealm.admin().users().userProfile().update(upConfig); } + @Test + public void testConditionForAnyValuedAttribute() { + createWorkflow(List.of()); + assertUserAttribute("user-1", true, "singleValue"); + assertUserAttribute("user-2", true, "v1", "v2", "v3"); + assertUserAttribute("user-3", false); + } + @Test public void testConditionForSingleValuedAttribute() { String expected = "valid";