validate configuration before saving it

This commit is contained in:
kritzl 2026-02-21 15:46:41 +01:00
commit 88ffb9b9f4
Signed by: kritzl
SSH key fingerprint: SHA256:5BmINP9VjZWaUk5Z+2CTut1KFhwLtd0ZynMekKbtViM
2 changed files with 75 additions and 22 deletions

View file

@ -1,15 +1,23 @@
package de.ccc.hamburg.keycloak.attribute_endpoints; 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.Config;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder; 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.UiPageProvider;
import org.keycloak.services.ui.extend.UiPageProviderFactory; import org.keycloak.services.ui.extend.UiPageProviderFactory;
import org.keycloak.userprofile.UserProfileProvider;
import java.util.List; import com.google.auto.service.AutoService;
/** /**
* Implements UiPageProvider to show a config page in the admin * Implements UiPageProvider to show a config page in the admin
@ -29,55 +37,108 @@ public class AdminUiPage implements UiPageProvider, UiPageProviderFactory<Compon
@Override @Override
public void close() { public void close() {
} }
@Override @Override
public String getId() { public String getId() {
return PROVIDER_ID; return PROVIDER_ID;
} }
@Override
public String getHelpText() { public String getHelpText() {
return "Configure endpoints of the Attribute Endpoint Provider."; 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()) {
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 @Override
public List<ProviderConfigProperty> getConfigProperties() { public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create() return ProviderConfigurationBuilder.create()
.property() .property()
.name("slug") .name("slug")
.label("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) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.property() .property()
.name("attribute-group") .name("attribute-group")
.label("Attribute Group") .label("Attribute Group")
.helpText("The attribute group to export.") .helpText("The attribute group to export.")
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.property() .property()
.name("match-role") .name("match-role")
.label("Match Role") .label("Match Role")
.helpText("Export only attributes of users with this role.") .helpText("Export only attributes of users with this role.")
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.property() .property()
.name("auth-role") .name("auth-role")
.label("Auth Role") .label("Auth Role")
.helpText("Role needeed by the authenticated account to be able to use this endpoint.") .helpText("Role needeed by the authenticated account to be able to use this endpoint.")
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.property() .property()
.name("attribute-regex") .name("attribute-regex")
.label("Attribute RegEx") .label("Attribute RegEx")
.helpText("A RegEx Rule used to verify each attribute value. Only matching values are returned.") .helpText("A RegEx Rule used to verify each attribute value. Only matching values are returned.")
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.build(); .build();
} }
} }

View file

@ -2,7 +2,6 @@ package de.ccc.hamburg.keycloak.attribute_endpoints;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; 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.services.resource.RealmResourceProvider;
import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.UserProfileProvider;
import io.quarkus.security.UnauthorizedException; import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
@ -102,16 +101,9 @@ public class AttributeEndpointsResourceProvider implements RealmResourceProvider
} }
} }
try UserModel authUser = auth.getUser();
if (!authUser.hasRole(authRole)) {
{ throw new ForbiddenException("User does not have required auth role.");
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();
} }
if (componentList.size() > 1) { if (componentList.size() > 1) {