Configure Extension with UiPageProvider #1

Open
kritzl wants to merge 6 commits from addUiPageProvider into main
2 changed files with 75 additions and 22 deletions
Showing only changes of commit 88ffb9b9f4 - Show all commits

validate configuration before saving it

kritzl 2026-02-21 15:46:41 +01:00
Signed by: kritzl
SSH key fingerprint: SHA256:5BmINP9VjZWaUk5Z+2CTut1KFhwLtd0ZynMekKbtViM

View file

@ -1,15 +1,23 @@
package de.ccc.hamburg.keycloak.attribute_endpoints;
import com.google.auto.service.AutoService;
import java.util.List;
import java.util.regex.Pattern;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.ui.extend.UiPageProvider;
import org.keycloak.services.ui.extend.UiPageProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;

I think getting rid of the emojis, while they are fun, might be a good idea.

I think getting rid of the emojis, while they are fun, might be a good idea.
import java.util.List;
import com.google.auto.service.AutoService;
/**
* Implements UiPageProvider to show a config page in the admin
@ -29,55 +37,108 @@ public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<Compon
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "Configure endpoints of the Attribute Endpoint Provider.";
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) {
String errorString = "\n";
Boolean hasError = false;
Pattern slugPattern = Pattern.compile("^[a-zA-Z0-9_-]*$");
String configAttributeSlug = model.getConfig().getFirst("slug");
if (!slugPattern.matcher(configAttributeSlug).matches()) {

Currently this gives us an ugly exception in the Keycloak logs for an empty slug, as configAttributeSlug is then null. Not critical as it still just errors, but we should probably check, if configAttributeSlug is null. I guess then adding a null check to all the other config$things is probably a good idea as well.

Currently this gives us an ugly exception in the Keycloak logs for an empty slug, as `configAttributeSlug` is then `null`. Not critical as it still just errors, but we should probably check, if `configAttributeSlug` is `null`. I guess then adding a null check to all the other `config$thing`s is probably a good idea as well.
hasError = true;
errorString += " • [Slug] can only contain anlphanumeric characters, dash and underscore (a-z A-Z 0-9 _ - )\n";
}
String configAuthRole = model.getConfig().getFirst("auth-role");
RoleModel authRole = realm.getRole(configAuthRole);
if (authRole == null) {
hasError = true;
errorString += " • [Auth Role] does not exist\n";
}
String configMatchRole = model.getConfig().getFirst("match-role");
RoleModel matchRole = realm.getRole(configMatchRole);
if (matchRole == null) {
hasError = true;
errorString += " • [Match Role] does not exist\n";
}
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
UPConfig upconfig = profileProvider.getConfiguration();
String configAttributeGroup = model.getConfig().getFirst("attribute-group");
if (!upconfig.getGroups().stream().anyMatch(g -> g.getName().equals(configAttributeGroup))) {
hasError = true;
errorString += " • [Attribute Group] does not exist\n";
}
String configAttributeRegex = model.getConfig().getFirst("attribute-regex");
Boolean regexIsBlank = configAttributeRegex == null;
if (!regexIsBlank) {
try {
Pattern.compile(configAttributeRegex);
} catch (Exception e) {
hasError = true;
errorString += " • [Attribute RegEx] is not a valid regex pattern\n";
}
}
if (hasError) {
throw new ComponentValidationException(errorString);
}
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property()
.name("slug")
.label("Slug")
.helpText("The slug in the path of the API endpoint (e.g. /realms/:realm/attribute-endpoint-provider/export/:slug)")
.helpText(
"The slug in the path of the API endpoint (e.g. /realms/:realm/attribute-endpoint-provider/export/:slug)")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property()
.name("attribute-group")
.label("Attribute Group")
.helpText("The attribute group to export.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property()
.name("match-role")
.label("Match Role")
.helpText("Export only attributes of users with this role.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property()
.name("auth-role")
.label("Auth Role")
.helpText("Role needeed by the authenticated account to be able to use this endpoint.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property()
.name("attribute-regex")
.label("Attribute RegEx")
.helpText("A RegEx Rule used to verify each attribute value. Only matching values are returned.")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.build();
}
}

View file

@ -2,7 +2,6 @@ package de.ccc.hamburg.keycloak.attribute_endpoints;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@ -23,7 +22,7 @@ import org.keycloak.services.managers.AuthenticationManager.AuthResult;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.userprofile.UserProfileProvider;
import io.quarkus.security.UnauthorizedException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.NotFoundException;
@ -102,16 +101,9 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
}
}
try
{
UserModel user = auth.getUser();
if (!user.hasRole(authRole)) {
throw new UnauthorizedException("User does not have required auth role.");
}
} catch (Exception e) {
System.err.println(e);
return Response.status(403, e.getMessage()).build();
UserModel authUser = auth.getUser();
if (!authUser.hasRole(authRole)) {
throw new ForbiddenException("User does not have required auth role.");
}
if (componentList.size() > 1) {