Add returning attributes split by name
Adds an endpoint that returns the list of attribute values as a map of attribute names in the requested attribute group. This is useful for requesting the mailing list addresses, where the individual attributes apply to different mailing lists.
This commit is contained in:
parent
2c3c0a3dab
commit
464c02dfb0
1 changed files with 155 additions and 98 deletions
|
|
@ -1,21 +1,11 @@
|
|||
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.UPConfig;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
|
|
@ -23,16 +13,13 @@ 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.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AttributeEndpointsResourceProvider implements RealmResourceProvider {
|
||||
private static final Logger LOG = Logger.getLogger(AttributeEndpointsResourceProvider.class);
|
||||
|
|
@ -51,83 +38,21 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
|
|||
public void close() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all atrribute 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<ComponentModel> 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)) {
|
||||
throw new ForbiddenException("User does not have required auth role.");
|
||||
}
|
||||
|
||||
List<String> attributeNames = upconfig.getAttributes()
|
||||
.stream()
|
||||
.filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup))
|
||||
.map(a -> a.getName())
|
||||
.toList();
|
||||
|
||||
UserProvider userProvider = session.users();
|
||||
// getRoleMembersStream only returns users with matchRole assigned directly; it misses
|
||||
// users who have the role via group membership (incl. parent groups) or composite roles.
|
||||
// hasRole() resolves the role the same way authUser.hasRole(authRole) does above.
|
||||
Stream<UserModel> users = userProvider.searchForUserStream(realm, Map.of())
|
||||
.filter(user -> user.hasRole(matchRole));
|
||||
|
||||
List<String> attribute_list = users
|
||||
List<String> attribute_list = ctx.users
|
||||
.map(user -> {
|
||||
Stream<String> attributeStream = attributeNames.stream()
|
||||
Stream<String> attributeStream = ctx.attributeNames.stream()
|
||||
.map(attributeName -> user.getAttributeStream(attributeName).toList())
|
||||
.flatMap(Collection::stream);
|
||||
|
||||
|
|
@ -137,17 +62,149 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
|
|||
})
|
||||
.flatMap(List::stream)
|
||||
.filter(attribute -> {
|
||||
if (regexIsBlank) {
|
||||
if (ctx.regexIsBlank) {
|
||||
return true;
|
||||
}
|
||||
final Pattern pattern = Pattern.compile(configAttributeRegex);
|
||||
final Pattern pattern = Pattern.compile(ctx.configAttributeRegex);
|
||||
final Matcher matcher = pattern.matcher(attribute);
|
||||
return matcher.find();
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Response.ok(attribute_list).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map of lists of attribute values. For each attribute in the named attribute
|
||||
* group, add an entry in the resulting map with the attribute name as the key, and the
|
||||
* values as a list as the values.
|
||||
*
|
||||
* @param attributeGroupName attribute group name
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("export/{attributeGroupName}/map")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response exportAttributeValuesMap(@PathParam("attributeGroupName") String attributeGroupName) {
|
||||
AttributeExportContext ctx = new AttributeExportContext(attributeGroupName);
|
||||
|
||||
List<UserModel> userList = ctx.users.toList();
|
||||
|
||||
Map<String, List<String>> attributeMap = ctx.attributeNames.stream()
|
||||
.collect(Collectors.toMap(
|
||||
attributeName -> attributeName,
|
||||
attributeName -> userList.stream()
|
||||
.flatMap(user -> user.getAttributeStream(attributeName))
|
||||
.filter(attribute -> !attribute.isEmpty())
|
||||
.filter(attribute -> {
|
||||
if (ctx.regexIsBlank) {
|
||||
return true;
|
||||
}
|
||||
final Pattern pattern = Pattern.compile(ctx.configAttributeRegex);
|
||||
final Matcher matcher = pattern.matcher(attribute);
|
||||
return matcher.find();
|
||||
})
|
||||
.toList()));
|
||||
|
||||
return Response.ok(attributeMap).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 {
|
||||
KeycloakContext context;
|
||||
RealmModel realm;
|
||||
List<ComponentModel> componentList;
|
||||
Auth auth;
|
||||
ComponentModel component;
|
||||
String configAuthRole;
|
||||
RoleModel authRole;
|
||||
String configMatchRole;
|
||||
RoleModel matchRole;
|
||||
UserProfileProvider profileProvider;
|
||||
UPConfig upconfig;
|
||||
String configAttributeGroup;
|
||||
String configAttributeRegex;
|
||||
Boolean regexIsBlank;
|
||||
UserModel authUser;
|
||||
List<String> attributeNames;
|
||||
UserProvider userProvider;
|
||||
Stream<UserModel> users;
|
||||
|
||||
AttributeExportContext(String slug) {
|
||||
context = session.getContext();
|
||||
realm = context.getRealm();
|
||||
|
||||
componentList = realm.getComponentsStream()
|
||||
.filter(c -> c.getProviderId().equals(AdminUiPage.PROVIDER_ID))
|
||||
.filter(c -> c.getConfig().getFirst("slug").equals(slug))
|
||||
.toList();
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
component = componentList.get(0);
|
||||
|
||||
configAuthRole = component.getConfig().getFirst("auth-role");
|
||||
authRole = realm.getRole(configAuthRole);
|
||||
if (authRole == null) {
|
||||
throw new ServerErrorException("Endpoint Configuration Error - auth-role does not exist.", 500);
|
||||
}
|
||||
|
||||
configMatchRole = component.getConfig().getFirst("match-role");
|
||||
matchRole = realm.getRole(configMatchRole);
|
||||
if (matchRole == null) {
|
||||
throw new ServerErrorException("Endpoint Configuration Error - match-role does not exist.", 500);
|
||||
}
|
||||
|
||||
profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
upconfig = profileProvider.getConfiguration();
|
||||
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);
|
||||
}
|
||||
|
||||
configAttributeRegex = component.getConfig().getFirst("attribute-regex");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
authUser = auth.getUser();
|
||||
if (!authUser.hasRole(authRole)) {
|
||||
throw new ForbiddenException("User does not have required auth role.");
|
||||
}
|
||||
|
||||
attributeNames = upconfig.getAttributes()
|
||||
.stream()
|
||||
.filter(a -> a.getGroup() != null && a.getGroup().equals(configAttributeGroup))
|
||||
.map(a -> a.getName())
|
||||
.toList();
|
||||
|
||||
userProvider = session.users();
|
||||
// getRoleMembersStream only returns users with matchRole assigned directly; it misses
|
||||
// users who have the role via group membership (incl. parent groups) or composite roles.
|
||||
// hasRole() resolves the role the same way authUser.hasRole(authRole) does above.
|
||||
users = userProvider.searchForUserStream(realm, Map.of())
|
||||
.filter(user -> user.hasRole(matchRole));
|
||||
}
|
||||
}
|
||||
|
||||
private static Auth getAuth(KeycloakSession session) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue