Validate client policy condition configuration

Closes #40187

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
(cherry picked from commit b9033ad9c38bacd16e205866c8891b6df6a210d7)
This commit is contained in:
Giuseppe Graziano 2025-06-05 16:09:02 +02:00 committed by Peter Skopek
parent 0074fab5c6
commit 1b3541ed15
4 changed files with 92 additions and 0 deletions

View File

@ -19,9 +19,13 @@ package org.keycloak.services.clientpolicy.condition;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
@ -32,4 +36,15 @@ public interface ClientPolicyConditionProviderFactory extends ProviderFactory<Cl
default boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_POLICIES);
}
/**
* Called before a Client Policy is created or updated. Allows you to validate the configuration
*
* @param session
* @param realm
* @param conditionRepresentation
* @throws ClientPolicyException
*/
default void validateConfiguration(KeycloakSession session, RealmModel realm, ClientPolicyConditionRepresentation conditionRepresentation) throws ClientPolicyException {
}
}

View File

@ -39,6 +39,8 @@ import org.keycloak.component.JsonConfigComponentModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
@ -49,6 +51,7 @@ import org.keycloak.representations.idm.ClientProfileRepresentation;
import org.keycloak.representations.idm.ClientProfilesRepresentation;
import org.keycloak.securityprofile.SecurityProfileProvider;
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider;
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
import org.keycloak.util.JsonSerialization;
@ -531,6 +534,9 @@ public class ClientPoliciesUtil {
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_POLICIES) && !isValidCondition(session, condition.getConditionProviderId())) {
throw new ClientPolicyException("Policy " + proposedPolicyRep.getName() + " contains invalid condition " + condition.getConditionProviderId());
}
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_POLICIES)) {
validateConditionConfig(session, condition);
}
policyRep.getConditions().add(condition);
}
}
@ -597,6 +603,30 @@ public class ClientPoliciesUtil {
return false;
}
private static void validateConditionConfig(KeycloakSession session, ClientPolicyConditionRepresentation conditionRep) throws ClientPolicyException {
ClientPolicyConditionProviderFactory factory = getClientPolicyConditionFactory(session, conditionRep.getConditionProviderId());
try {
factory.validateConfiguration(session, session.getContext().getRealm(), conditionRep);
}
catch (ClientPolicyException e) {
throw new ClientPolicyException("Invalid " + conditionRep.getConditionProviderId() + " configuration - " + e.getMessage());
}
}
private static ClientPolicyConditionProviderFactory getClientPolicyConditionFactory(KeycloakSession session, String providerId) {
Class<? extends Provider> provider = session.getProviderClass(ClientPolicyConditionProvider.class.getName());
if (provider == null) {
throw new IllegalArgumentException("Invalid provider type '" + ClientPolicyConditionProvider.class.getName() + "'");
}
ProviderFactory<? extends Provider> f = session.getKeycloakSessionFactory().getProviderFactory(provider, providerId);
if (f == null) {
throw new IllegalArgumentException("No such provider '" + providerId + "'");
}
return (ClientPolicyConditionProviderFactory) f;
}
static String getClientProfilesJsonString(RealmModel realm) {
return realm.getAttribute(Constants.CLIENT_PROFILES);
}

View File

@ -22,8 +22,13 @@ import java.util.Arrays;
import java.util.List;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
@ -73,4 +78,13 @@ public class ClientScopesConditionFactory extends AbstractClientPolicyConditionP
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ClientPolicyConditionRepresentation conditionRepresentation) throws ClientPolicyException {
ClientScopesCondition.Configuration configuration = JsonSerialization.mapper.convertValue(conditionRepresentation.getConfiguration(), ClientScopesCondition.Configuration.class);
if (!realm.getClientScopesStream().map(ClientScopeModel::getName).toList().containsAll(configuration.getScopes())) {
throw new ClientPolicyException("Client scopes not allowed: " + configuration.getScopes());
}
}
}

View File

@ -37,6 +37,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import jakarta.ws.rs.BadRequestException;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
@ -53,9 +54,11 @@ import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
@ -71,6 +74,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesCond
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LogoutConfirmPage;
@ -84,6 +88,7 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.JsonSerialization;
/**
* This test class is for testing a condition of client policies.
@ -444,6 +449,34 @@ public class ClientPoliciesConditionTest extends AbstractClientPoliciesTest {
}
@Test
public void testClientScopesConditionValidation() throws Exception {
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Het Eerste Profiel")
.addExecutor(PKCEEnforcerExecutorFactory.PROVIDER_ID,
createPKCEEnforceExecutorConfig(Boolean.TRUE))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "test", Boolean.TRUE)
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.ANY, List.of("fake-client-scope")))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
ClientPoliciesRepresentation clientPolicies = json==null ? null : JsonSerialization.readValue(json, ClientPoliciesRepresentation.class);
BadRequestException e = Assert.assertThrows(BadRequestException.class,
() -> adminClient.realm(REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(clientPolicies));
ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class);
Assert.assertEquals("Invalid client-scopes configuration - Client scopes not allowed: [fake-client-scope]", error.getErrorMessage());
}
@Test
public void testClientAttributesCondition() throws Exception {
// register profiles