Mapper option 'Aggregate attribute values' is now applied to group hierarchy (#7871)

Closes #11255
This commit is contained in:
Jurjan-Paul Medema 2022-09-12 13:34:28 +02:00 committed by GitHub
parent 68140dfb1f
commit eb0124e3e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 215 additions and 9 deletions

View file

@ -488,13 +488,13 @@ public final class KeycloakModelUtils {
}
public static List<String> resolveAttribute(GroupModel group, String name) {
List<String> values = group.getAttributeStream(name).collect(Collectors.toList());
if (!values.isEmpty()) return values;
if (group.getParentId() == null) return null;
return resolveAttribute(group.getParent(), name);
public static Collection<String> resolveAttribute(GroupModel group, String name, boolean aggregateAttrs) {
Set<String> values = group.getAttributeStream(name).collect(Collectors.toSet());
if ((values.isEmpty() || aggregateAttrs) && group.getParentId() != null) {
values.addAll(resolveAttribute(group.getParent(), name, aggregateAttrs));
}
return values;
}
public static Collection<String> resolveAttribute(UserModel user, String name, boolean aggregateAttrs) {
List<String> values = user.getAttributeStream(name).collect(Collectors.toList());
@ -505,13 +505,13 @@ public final class KeycloakModelUtils {
}
aggrValues.addAll(values);
}
Stream<List<String>> attributes = user.getGroupsStream()
.map(group -> resolveAttribute(group, name))
Stream<Collection<String>> attributes = user.getGroupsStream()
.map(group -> resolveAttribute(group, name, aggregateAttrs))
.filter(Objects::nonNull)
.filter(attr -> !attr.isEmpty());
if (!aggregateAttrs) {
Optional<List<String>> first = attributes.findFirst();
Optional<Collection<String>> first = attributes.findFirst();
if (first.isPresent()) return first.get();
} else {
aggrValues.addAll(attributes.flatMap(Collection::stream).collect(Collectors.toSet()));

View file

@ -1271,6 +1271,212 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
}
}
// KEYCLOAK-17252 -- Test the scenario where:
// - one group is a subgroup of another
// - only the parent group has values for the 'group-value' attribute
// - a user is a member of the subgroup
// - the 'single value' attribute 'group-value' should not be aggregated
@Test
public void testGroupAttributeTwoGroupHierarchyNoMultivalueNoAggregateFromParent() throws Exception {
// get the user
UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost");
// create two groups with two values (one is the same value)
GroupRepresentation group1 = new GroupRepresentation();
group1.setName("group1");
group1.setAttributes(new HashMap<>());
group1.getAttributes().put("group-value", Arrays.asList("value1", "value2"));
adminClient.realm("test").groups().add(group1);
group1 = adminClient.realm("test").getGroupByPath("/group1");
GroupRepresentation group2 = new GroupRepresentation();
group2.setName("group2");
group2.setAttributes(new HashMap<>());
adminClient.realm("test").groups().add(group2);
group2 = adminClient.realm("test").getGroupByPath("/group2");
// make group2 a subgroup of group1 and make user join group2
adminClient.realm("test").groups().group(group1.getId()).subGroup(group2);
userResource.joinGroup(group2.getId());
// create the attribute mapper
ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, false, false)).close();
try {
// test it
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNotNull(idToken.getOtherClaims());
assertNotNull(idToken.getOtherClaims().get("group-value"));
assertTrue(idToken.getOtherClaims().get("group-value") instanceof String);
assertTrue("value1".equals(idToken.getOtherClaims().get("group-value"))
|| "value2".equals(idToken.getOtherClaims().get("group-value")));
} finally {
// revert
userResource.leaveGroup(group2.getId());
adminClient.realm("test").groups().group(group2.getId()).remove();
adminClient.realm("test").groups().group(group1.getId()).remove();
deleteMappers(protocolMappers);
}
}
// KEYCLOAK-17252 -- Test the scenario where:
// - one group is a subgroup of another
// - both groups have values for the 'group-value' attribute
// - a user is a member of the subgroup
// - the 'single value' attribute 'group-value' should not be aggregated
@Test
public void testGroupAttributeTwoGroupHierarchyNoMultivalueNoAggregateFromChild() throws Exception {
// get the user
UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost");
// create two groups with two values (one is the same value)
GroupRepresentation group1 = new GroupRepresentation();
group1.setName("group1");
group1.setAttributes(new HashMap<>());
group1.getAttributes().put("group-value", Arrays.asList("value1", "value2"));
adminClient.realm("test").groups().add(group1);
group1 = adminClient.realm("test").getGroupByPath("/group1");
GroupRepresentation group2 = new GroupRepresentation();
group2.setName("group2");
group2.setAttributes(new HashMap<>());
group2.getAttributes().put("group-value", Arrays.asList("value3", "value4"));
adminClient.realm("test").groups().add(group2);
group2 = adminClient.realm("test").getGroupByPath("/group2");
// make group2 a subgroup of group1 and make user join group2
adminClient.realm("test").groups().group(group1.getId()).subGroup(group2);
userResource.joinGroup(group2.getId());
// create the attribute mapper
ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, false, false)).close();
try {
// test it
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNotNull(idToken.getOtherClaims());
assertNotNull(idToken.getOtherClaims().get("group-value"));
assertTrue(idToken.getOtherClaims().get("group-value") instanceof String);
assertTrue("value3".equals(idToken.getOtherClaims().get("group-value"))
|| "value4".equals(idToken.getOtherClaims().get("group-value")));
} finally {
// revert
userResource.leaveGroup(group2.getId());
adminClient.realm("test").groups().group(group2.getId()).remove();
adminClient.realm("test").groups().group(group1.getId()).remove();
deleteMappers(protocolMappers);
}
}
// KEYCLOAK-17252 -- Test the scenario where:
// - one group is a subgroup of another
// - both groups have values for the 'group-value' attribute
// - a user is a member of the subgroup
// - the multivalue attribute 'group-value' should not be aggregated
@Test
public void testGroupAttributeTwoGroupHierarchyMultiValueNoAggregate() throws Exception {
// get the user
UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost");
// create two groups with two values (one is the same value)
GroupRepresentation group1 = new GroupRepresentation();
group1.setName("group1");
group1.setAttributes(new HashMap<>());
group1.getAttributes().put("group-value", Arrays.asList("value1", "value2"));
adminClient.realm("test").groups().add(group1);
group1 = adminClient.realm("test").getGroupByPath("/group1");
GroupRepresentation group2 = new GroupRepresentation();
group2.setName("group2");
group2.setAttributes(new HashMap<>());
group2.getAttributes().put("group-value", Arrays.asList("value2", "value3"));
adminClient.realm("test").groups().add(group2);
group2 = adminClient.realm("test").getGroupByPath("/group2");
// make group2 a subgroup of group1 and make user join group2
adminClient.realm("test").groups().group(group1.getId()).subGroup(group2);
userResource.joinGroup(group2.getId());
// create the attribute mapper
ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, false)).close();
try {
// test it
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNotNull(idToken.getOtherClaims());
assertNotNull(idToken.getOtherClaims().get("group-value"));
assertTrue(idToken.getOtherClaims().get("group-value") instanceof List);
assertEquals(2, ((List) idToken.getOtherClaims().get("group-value")).size());
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2"));
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value3"));
} finally {
// revert
userResource.leaveGroup(group2.getId());
adminClient.realm("test").groups().group(group2.getId()).remove();
adminClient.realm("test").groups().group(group1.getId()).remove();
deleteMappers(protocolMappers);
}
}
// KEYCLOAK-17252 -- Test the scenario where:
// - one group is a subgroup of another
// - both groups have values for the 'group-value' attribute
// - a user is a member of the subgroup
// - the multivalue attribute 'group-value' should be aggregated
@Test
public void testGroupAttributeTwoGroupHierarchyMultiValueAggregate() throws Exception {
// get the user
UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost");
UserRepresentation user = userResource.toRepresentation();
user.setAttributes(new HashMap<>());
user.getAttributes().put("group-value", Arrays.asList("user-value1", "user-value2"));
userResource.update(user);
// create two groups with two values (one is the same value)
GroupRepresentation group1 = new GroupRepresentation();
group1.setName("group1");
group1.setAttributes(new HashMap<>());
group1.getAttributes().put("group-value", Arrays.asList("value1", "value2"));
adminClient.realm("test").groups().add(group1);
group1 = adminClient.realm("test").getGroupByPath("/group1");
GroupRepresentation group2 = new GroupRepresentation();
group2.setName("group2");
group2.setAttributes(new HashMap<>());
group2.getAttributes().put("group-value", Arrays.asList("value2", "value3"));
adminClient.realm("test").groups().add(group2);
group2 = adminClient.realm("test").getGroupByPath("/group2");
// make group2 a subgroup of group1 and make user join group2
adminClient.realm("test").groups().group(group1.getId()).subGroup(group2);
userResource.joinGroup(group2.getId());
// create the attribute mapper
ProtocolMappersResource protocolMappers = findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(createClaimMapper("group-value", "group-value", "group-value", "String", true, true, true, true)).close();
try {
// test it
OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNotNull(idToken.getOtherClaims());
assertNotNull(idToken.getOtherClaims().get("group-value"));
assertTrue(idToken.getOtherClaims().get("group-value") instanceof List);
assertEquals(5, ((List) idToken.getOtherClaims().get("group-value")).size());
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value1"));
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("user-value2"));
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value1"));
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value2"));
assertTrue(((List) idToken.getOtherClaims().get("group-value")).contains("value3"));
} finally {
// revert
user.getAttributes().remove("group-value");
userResource.update(user);
userResource.leaveGroup(group2.getId());
adminClient.realm("test").groups().group(group2.getId()).remove();
adminClient.realm("test").groups().group(group1.getId()).remove();
deleteMappers(protocolMappers);
}
}
@Test
@EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true)
public void executeTokenMappersOnDynamicScopes() {