From e5d68bb9ad50edaa7c9fc4d2cdf96d77c8f77808 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Sun, 19 Jul 2026 10:45:08 +0200 Subject: [PATCH] Add just the refactoring from #26. This is based off #31, so that needs to be merged first. --- .../AttributeEndpointsResourceProvider.java | 216 +++++++++--------- 1 file changed, 112 insertions(+), 104 deletions(-) diff --git a/attribute-endpoints-provider/src/main/java/de/ccc/hamburg/keycloak/attribute_endpoints/AttributeEndpointsResourceProvider.java b/attribute-endpoints-provider/src/main/java/de/ccc/hamburg/keycloak/attribute_endpoints/AttributeEndpointsResourceProvider.java index 15303dc..6c069c0 100644 --- a/attribute-endpoints-provider/src/main/java/de/ccc/hamburg/keycloak/attribute_endpoints/AttributeEndpointsResourceProvider.java +++ b/attribute-endpoints-provider/src/main/java/de/ccc/hamburg/keycloak/attribute_endpoints/AttributeEndpointsResourceProvider.java @@ -1,21 +1,12 @@ package de.ccc.hamburg.keycloak.attribute_endpoints; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakContext; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserProvider; +import org.keycloak.models.*; +import org.keycloak.representations.userprofile.config.UPAttribute; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; @@ -23,16 +14,11 @@ import org.keycloak.services.managers.AuthenticationManager.AuthResult; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.userprofile.UserProfileProvider; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.ServerErrorException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Stream; public class AttributeEndpointsResourceProvider implements RealmResourceProvider { private static final Logger LOG = Logger.getLogger(AttributeEndpointsResourceProvider.class); @@ -51,81 +37,21 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider public void close() { } + /** + * Returns a list of all attribute values selected by the attribute group attributeGroupName. + * + * @param attributeGroupName attribute group name + * @return a list of attribute values. + */ @GET @Path("export/{slug}") @Produces(MediaType.APPLICATION_JSON) - public Response exportAttributeValues(@PathParam("slug") String slug) { - KeycloakContext context = session.getContext(); - RealmModel realm = context.getRealm(); + public Response exportAttributeValues(@PathParam("slug") String attributeGroupName) { + AttributeExportContext ctx = new AttributeExportContext(attributeGroupName); - List componentList = realm.getComponentsStream() - .filter(c -> c.getProviderId().equals(AdminUiPage.PROVIDER_ID)) - .filter(c -> c.getConfig().getFirst("slug").equals(slug)) - .toList(); - - Auth auth = AttributeEndpointsResourceProvider.getAuth(session); - - if (componentList.isEmpty()) { - throw new NotFoundException("Endpoint not found."); - } - - if (componentList.size() > 1) { - throw new NotFoundException( - "Endpoint Configuration Error - Multiple configurations exist for this endpoint."); - } - - ComponentModel component = componentList.get(0); - - String configAuthRole = component.getConfig().getFirst("auth-role"); - RoleModel authRole = realm.getRole(configAuthRole); - if (authRole == null) { - throw new ServerErrorException("Endpoint Configuration Error - auth-role does not exist.", 500); - } - - String configMatchRole = component.getConfig().getFirst("match-role"); - RoleModel matchRole = realm.getRole(configMatchRole); - if (matchRole == null) { - throw new ServerErrorException("Endpoint Configuration Error - match-role does not exist.", 500); - } - - UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); - UPConfig upconfig = profileProvider.getConfiguration(); - String configAttributeGroup = component.getConfig().getFirst("attribute-group"); - if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) { - throw new ServerErrorException("Endpoint Configuration Error - attribute-group does not exist.", 500); - } - - String configAttributeRegex = component.getConfig().getFirst("attribute-regex"); - Boolean regexIsBlank = configAttributeRegex == null; - - if (!regexIsBlank) { - try { - Pattern.compile(configAttributeRegex); - } catch (Exception e) { - throw new ServerErrorException( - "Endpoint Configuration Error - attribute-regex is not a valid regex pattern.", 500); - } - } - - UserModel authUser = auth.getUser(); - if (!authUser.hasRole(authRole)) { - LOG.info("User " + authUser.getUsername() + " does not have required role " + authRole.getName()); - throw new ForbiddenException("User does not have required auth role."); - } - - List attributeNames = upconfig.getAttributes() - .stream() - .filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup)) - .map(a -> a.getName()) - .toList(); - - UserProvider userProvider = session.users(); - Stream users = userProvider.searchForUserStream(realm, Map.of()) - .filter(user -> user.hasRole(matchRole)); - - List attribute_list = users + List attribute_list = ctx.users .map(user -> { - Stream attributeStream = attributeNames.stream() + Stream attributeStream = ctx.attributeNames.stream() .map(attributeName -> user.getAttributeStream(attributeName).toList()) .flatMap(Collection::stream); @@ -134,18 +60,83 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider .toList(); }) .flatMap(List::stream) - .filter(attribute -> { - if (regexIsBlank) { - return true; - } - final Pattern pattern = Pattern.compile(configAttributeRegex); - final Matcher matcher = pattern.matcher(attribute); - return matcher.find(); - }) + .filter(ctx.filter::matches) .toList(); return Response.ok(attribute_list).build(); + } + /** + * Resolves and validates the configuration and request state needed to export attribute + * values for a given slug, exposing the results as member variables. + */ + private class AttributeExportContext { + List attributeNames; + UserModel authUser; + String configAttributeGroup; + RegExFilter filter; + UPConfig upconfig; + Stream users; + + AttributeExportContext(String slug) { + RealmModel realm = session.getContext().getRealm(); + + List componentList = realm.getComponentsStream() + .filter(c -> c.getProviderId().equals(AdminUiPage.PROVIDER_ID)) + .filter(c -> c.getConfig().getFirst("slug").equals(slug)) + .toList(); + + if (componentList.isEmpty()) { + throw new NotFoundException("Endpoint not found"); + } + + if (componentList.size() > 1) { + throw new NotFoundException( + "Endpoint Configuration Error - Multiple configurations exist for this endpoint."); + } + + ComponentModel component = componentList.get(0); + + RoleModel authRole = realm.getRole(component.getConfig().getFirst("auth-role")); + if (authRole == null) { + throw new ServerErrorException("Endpoint Configuration Error - auth-role does not exist.", 500); + } + + RoleModel matchRole = realm.getRole(component.getConfig().getFirst("match-role")); + if (matchRole == null) { + throw new ServerErrorException("Endpoint Configuration Error - match-role does not exist.", 500); + } + + upconfig = session.getProvider(UserProfileProvider.class).getConfiguration(); + configAttributeGroup = component.getConfig().getFirst("attribute-group"); + if (upconfig.getGroups().stream().noneMatch(g -> g.getName().equals(configAttributeGroup))) { + throw new ServerErrorException("Endpoint Configuration Error - attribute-group does not exist.", 500); + } + + try { + filter = new RegExFilter(component.getConfig().getFirst("attribute-regex")); + } catch (Exception e) { + throw new ServerErrorException( + "Endpoint Configuration Error - attribute-regex is not a valid regex pattern.", 500); + } + + authUser = AttributeEndpointsResourceProvider.getAuth(session).getUser(); + if (!authUser.hasRole(authRole)) { + LOG.info("User " + authUser.getUsername() + " does not have required role " + authRole.getName() + " for attribute endpoint " + slug); + throw new ForbiddenException("User does not have required auth role."); + } + + // select all attributes that match configAttributeGroup, or that are in a group that matches configAttributeGroup + attributeNames = upconfig.getAttributes() + .stream() + .filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup)) + .map(UPAttribute::getName) + .toList(); + + UserProvider userProvider = session.users(); + users = userProvider.searchForUserStream(realm, Map.of()) + .filter(user -> user.hasRole(matchRole)); + } } private static Auth getAuth(KeycloakSession session) { @@ -156,8 +147,25 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider } RealmModel realm = session.getContext().getRealm(); - ClientModel client = auth.getClient(); - return new Auth(realm, auth.getToken(), auth.getUser(), client, auth.getSession(), false); + ClientModel client = auth.client(); + return new Auth(realm, auth.token(), auth.user(), client, auth.session(), false); + } + + private static class RegExFilter { + Pattern pattern; + + RegExFilter(String regex) { + if (regex == null) + pattern = null; + else + pattern = Pattern.compile(regex); + } + + boolean matches(String input) { + if (pattern == null) + return true; + return pattern.matcher(input).matches(); + } } }