Extend client policies tests to check exact events (#46093)

Closes #46114

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš 2026-02-13 15:47:38 +01:00 committed by GitHub
parent d2150a19d5
commit 74988b5c0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 310 additions and 56 deletions

View file

@ -41,6 +41,11 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-v2-rest</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.tests</groupId>
<artifactId>keycloak-tests-custom-providers</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>

View file

@ -41,19 +41,26 @@ import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
import org.keycloak.representations.idm.ClientPolicyRepresentation;
import org.keycloak.representations.idm.ClientProfileRepresentation;
import org.keycloak.representations.idm.ClientProfilesRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextConditionFactory;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutor;
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectHttpClient;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testsuite.client.policies.TrackEventsClientPolicyExecutor;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.http.HttpMessage;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
@ -67,18 +74,21 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* Tests that client policies are properly executed when creating/updating clients via the Admin API v2.
*
* <p>
* These tests verify that the client policy framework correctly intercepts REGISTER, UPDATE, REGISTERED,
* and UPDATED events when clients are managed through the v2 API endpoints.
*
* <p>
* Note: Currently the v2 API creates clients in two phases:
* 1. Create a minimal client with just clientId and protocol
* 2. Update the client model with the full representation
*
* <p>
* This means client policies are triggered on the minimal representation during CREATE,
* which doesn't include the client authenticator type. The policy is then triggered again
* on UPDATE with the full model.
@ -96,8 +106,11 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
@InjectAdminClient
Keycloak adminClient;
@InjectRunOnServer
RunOnServerClient runOnServer;
@AfterEach
public void cleanup() throws Exception {
public void cleanup() {
// Clean up any test clients
cleanupClient("test-policy-client");
cleanupClient("test-auto-config-client");
@ -107,6 +120,8 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
// Revert to builtin profiles/policies
revertToBuiltinProfiles();
revertToBuiltinPolicies();
cleanupTrackEventsClientPolicyExecutor();
}
/**
@ -213,6 +228,17 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
}
}
private OIDCClientRepresentation getPutUpdateClientRep() {
OIDCClientRepresentation rep = new OIDCClientRepresentation();
rep.setClientId("test-put-update-client");
rep.setEnabled(true);
var auth = new OIDCClientRepresentation.Auth();
auth.setMethod(JWTClientSecretAuthenticator.PROVIDER_ID);
auth.setSecret("secret");
rep.setAuth(auth);
return rep;
}
/**
* Test that updating a client via PUT with an unacceptable client authenticator fails.
*/
@ -223,14 +249,7 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
setAuthHeader(createRequest);
createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = new OIDCClientRepresentation();
rep.setClientId("test-put-update-client");
rep.setEnabled(true);
var auth = new OIDCClientRepresentation.Auth();
auth.setMethod(JWTClientSecretAuthenticator.PROVIDER_ID);
auth.setSecret("secret");
rep.setAuth(auth);
OIDCClientRepresentation rep = getPutUpdateClientRep();
createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
try (var response = client.execute(createRequest)) {
@ -271,13 +290,7 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
setAuthHeader(createRequest);
createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = new OIDCClientRepresentation();
rep.setClientId("test-put-update-client");
rep.setEnabled(true);
var auth = new OIDCClientRepresentation.Auth();
auth.setMethod(JWTClientSecretAuthenticator.PROVIDER_ID);
auth.setSecret("secret");
rep.setAuth(auth);
OIDCClientRepresentation rep = getPutUpdateClientRep();
createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
@ -366,13 +379,7 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
setAuthHeader(createRequest);
createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = new OIDCClientRepresentation();
rep.setClientId("test-put-update-client");
rep.setEnabled(true);
var auth = new OIDCClientRepresentation.Auth();
auth.setMethod(JWTClientSecretAuthenticator.PROVIDER_ID);
auth.setSecret("secret");
rep.setAuth(auth);
OIDCClientRepresentation rep = getPutUpdateClientRep();
createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
@ -403,38 +410,181 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
}
}
/**
* GET /clients/{client}
* Policy Events: ClientPolicyEvent.VIEW
*/
@Test
public void getClientViewEvent() throws Exception {
setupAlwaysAppliedTestPolicy();
HttpGet getClient = new HttpGet(getClientsApiUrl() + "/account");
setAuthHeader(getClient);
try (var response = client.execute(getClient)) {
assertThat(response.getStatusLine().getStatusCode(), is(200));
}
assertClientPolicyEventIsEmitted(ClientPolicyEvent.VIEW);
}
/**
* POST /clients
* Policy Events: ClientPolicyEvent.REGISTER + ClientPolicyEvent.REGISTERED
*/
@Test
public void createClientRegisterEvent() throws Exception {
setupAlwaysAppliedTestPolicy();
HttpPost request = new HttpPost(getClientsApiUrl());
setAuthHeader(request);
request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = new OIDCClientRepresentation();
rep.setClientId("client-123");
request.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
try (var response = client.execute(request)) {
assertThat(response.getStatusLine().getStatusCode(), is(201));
EntityUtils.consumeQuietly(response.getEntity());
}
assertClientPolicyEventIsEmitted(ClientPolicyEvent.REGISTER, ClientPolicyEvent.REGISTERED);
}
/**
* PUT /clients/{client}
* Policy Events:
* - a) Create a new client via PUT: ClientPolicyEvent.REGISTER, ClientPolicyEvent.REGISTERED
* - b) Update the client: ClientPolicyEvent.UPDATE, ClientPolicyEvent.UPDATED
*/
@Test
public void updateClientUpdateEvent() throws Exception {
setupAlwaysAppliedTestPolicy();
HttpPut createRequest = new HttpPut(getClientsApiUrl() + "/test-put-update-client");
setAuthHeader(createRequest);
createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = getPutUpdateClientRep();
createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
try (var response = client.execute(createRequest)) {
assertEquals(201, response.getStatusLine().getStatusCode());
EntityUtils.consumeQuietly(response.getEntity());
}
assertClientPolicyEventIsEmitted(ClientPolicyEvent.REGISTER, ClientPolicyEvent.REGISTERED);
HttpPut updateRequest = new HttpPut(getClientsApiUrl() + "/test-put-update-client");
setAuthHeader(updateRequest);
updateRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
rep = getPutUpdateClientRep();
rep.setDescription("Updated");
updateRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
try (var response = client.execute(updateRequest)) {
assertEquals(200, response.getStatusLine().getStatusCode());
EntityUtils.consumeQuietly(response.getEntity());
}
// for now, the VIEW is also present, but it is not required for update
assertClientPolicyEventIsEmitted(ClientPolicyEvent.VIEW, ClientPolicyEvent.UPDATE, ClientPolicyEvent.UPDATED);
}
/**
* DELETE /clients/{client}
* Policy Events: ClientPolicyEvent.UNREGISTER
*/
@Test
public void deleteClientUnregisterEvent() throws Exception {
HttpPut createRequest = new HttpPut(getClientsApiUrl() + "/test-put-update-client");
setAuthHeader(createRequest);
createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
OIDCClientRepresentation rep = getPutUpdateClientRep();
createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep)));
try (var response = client.execute(createRequest)) {
assertEquals(201, response.getStatusLine().getStatusCode());
EntityUtils.consumeQuietly(response.getEntity());
}
setupAlwaysAppliedTestPolicy();
cleanupClient(rep.getClientId());
assertClientPolicyEventIsEmitted(ClientPolicyEvent.UNREGISTER);
}
private void assertClientPolicyEventIsEmitted(ClientPolicyEvent... events) {
runOnServer.run(session -> {
TrackEventsClientPolicyExecutor executor = (TrackEventsClientPolicyExecutor) session.getProvider(ClientPolicyExecutorProvider.class, TrackEventsClientPolicyExecutor.PROVIDER_ID);
assertNotNull(executor);
try {
var foundEvents = executor.getEvents();
assertNotNull(foundEvents);
assertThat(foundEvents, contains(events));
assertThat(foundEvents, hasSize(events.length));
} finally {
executor.clearEventResult();
}
});
}
private void cleanupTrackEventsClientPolicyExecutor() {
runOnServer.run(session -> {
TrackEventsClientPolicyExecutor executor = (TrackEventsClientPolicyExecutor) session.getProvider(ClientPolicyExecutorProvider.class, TrackEventsClientPolicyExecutor.PROVIDER_ID);
assertNotNull(executor);
executor.clearEventResult();
});
}
/**
* Sets up a policy that does NOT allow client_id and secret authenticator.
* Only JWT-based authenticators are allowed.
*/
private void setupPolicyClientIdAndSecretNotAcceptable() throws Exception {
setupPolicy("Test Profile/Policy that restricts client authenticators");
setupSecureClientAuthenticatorPolicy("Test Profile/Policy that restricts client authenticators");
}
/**
* Sets up a policy with auto-configuration that defaults to X509 authenticator.
*/
private void setupPolicyWithAutoConfiguration() throws Exception {
setupPolicy("Test Profile/Policy with auto-configuration - defaults to X509",
setupSecureClientAuthenticatorPolicy("Test Profile/Policy with auto-configuration - defaults to X509",
config -> config.setDefaultClientAuthenticator(X509ClientAuthenticator.PROVIDER_ID));
}
private void setupPolicy(String description) throws Exception {
setupPolicy(description, PROFILE_NAME, POLICY_NAME, (config) -> {
private void setupAlwaysAppliedTestPolicy() throws Exception {
ClientPolicyExecutorRepresentation executorRep = new ClientPolicyExecutorRepresentation();
executorRep.setExecutorProviderId(TrackEventsClientPolicyExecutor.PROVIDER_ID);
TrackEventsClientPolicyExecutor.Configuration config = new TrackEventsClientPolicyExecutor.Configuration();
JsonNode configNode = JsonSerialization.mapper.readValue(
JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class);
executorRep.setConfiguration(configNode);
ClientPolicyConditionRepresentation conditionRep = new ClientPolicyConditionRepresentation();
conditionRep.setConditionProviderId(AnyClientConditionFactory.PROVIDER_ID);
ClientPolicyConditionConfigurationRepresentation conditionConfig = new ClientPolicyConditionConfigurationRepresentation();
JsonNode conditionConfigNode = JsonSerialization.mapper.readValue(
JsonSerialization.mapper.writeValueAsBytes(conditionConfig), JsonNode.class);
conditionRep.setConfiguration(conditionConfigNode);
setupPolicy("Test Profile/Policy that handles the TrackEventsClientPolicyExecutor and verifies types", PROFILE_NAME, POLICY_NAME, executorRep, conditionRep);
}
private void setupSecureClientAuthenticatorPolicy(String description) throws Exception {
setupSecureClientAuthenticatorPolicy(description, (config) -> {
});
}
private void setupPolicy(String description, Consumer<SecureClientAuthenticatorExecutor.Configuration> configuration) throws Exception {
setupPolicy(description, PROFILE_NAME, POLICY_NAME, configuration);
}
private void setupPolicy(String description, String profileName, String policyName, Consumer<SecureClientAuthenticatorExecutor.Configuration> configuration) throws Exception {
// Create profile
ClientProfileRepresentation profileRep = new ClientProfileRepresentation();
profileRep.setName(profileName);
profileRep.setDescription(description);
profileRep.setExecutors(new ArrayList<>());
/**
* Setup secure client authenticator executor
*/
private void setupSecureClientAuthenticatorPolicy(String description, Consumer<SecureClientAuthenticatorExecutor.Configuration> configuration) throws Exception {
ClientPolicyExecutorRepresentation executorRep = new ClientPolicyExecutorRepresentation();
executorRep.setExecutorProviderId(SecureClientAuthenticatorExecutorFactory.PROVIDER_ID);
@ -450,7 +600,31 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
JsonNode configNode = JsonSerialization.mapper.readValue(
JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class);
executorRep.setConfiguration(configNode);
profileRep.getExecutors().add(executorRep);
// Add condition for authenticated user context
ClientPolicyConditionRepresentation conditionRep = new ClientPolicyConditionRepresentation();
conditionRep.setConditionProviderId(ClientUpdaterContextConditionFactory.PROVIDER_ID);
ClientPolicyConditionConfigurationRepresentation conditionConfig = new ClientPolicyConditionConfigurationRepresentation();
conditionConfig.setConfigAsMap(
ClientUpdaterContextConditionFactory.UPDATE_CLIENT_SOURCE,
List.of(ClientUpdaterContextConditionFactory.BY_AUTHENTICATED_USER)
);
JsonNode conditionConfigNode = JsonSerialization.mapper.readValue(
JsonSerialization.mapper.writeValueAsBytes(conditionConfig), JsonNode.class);
conditionRep.setConfiguration(conditionConfigNode);
setupPolicy(description, PROFILE_NAME, POLICY_NAME, executorRep, conditionRep);
}
private void setupPolicy(String description, String profileName, String policyName, ClientPolicyExecutorRepresentation executor, ClientPolicyConditionRepresentation condition) {
// Create profile
ClientProfileRepresentation profileRep = new ClientProfileRepresentation();
profileRep.setName(profileName);
profileRep.setDescription(description);
profileRep.setExecutors(new ArrayList<>());
profileRep.getExecutors().add(executor);
ClientProfilesRepresentation profilesRep = new ClientProfilesRepresentation();
profilesRep.setProfiles(List.of(profileRep));
@ -465,19 +639,7 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
policyRep.setProfiles(List.of(profileName));
policyRep.setConditions(new ArrayList<>());
// Add condition for authenticated user context
ClientPolicyConditionRepresentation conditionRep = new ClientPolicyConditionRepresentation();
conditionRep.setConditionProviderId(ClientUpdaterContextConditionFactory.PROVIDER_ID);
ClientPolicyConditionConfigurationRepresentation conditionConfig = new ClientPolicyConditionConfigurationRepresentation();
conditionConfig.setConfigAsMap(
ClientUpdaterContextConditionFactory.UPDATE_CLIENT_SOURCE,
List.of(ClientUpdaterContextConditionFactory.BY_AUTHENTICATED_USER)
);
JsonNode conditionConfigNode = JsonSerialization.mapper.readValue(
JsonSerialization.mapper.writeValueAsBytes(conditionConfig), JsonNode.class);
conditionRep.setConfiguration(conditionConfigNode);
policyRep.getConditions().add(conditionRep);
policyRep.getConditions().add(condition);
ClientPoliciesRepresentation policiesRep = new ClientPoliciesRepresentation();
policiesRep.setPolicies(List.of(policyRep));
@ -525,7 +687,8 @@ public class ClientPoliciesV2Test extends AbstractClientApiV2Test {
public static class AdminV2Config implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.CLIENT_ADMIN_API_V2);
return config.features(Profile.Feature.CLIENT_ADMIN_API_V2)
.dependency("org.keycloak.tests", "keycloak-tests-custom-providers");
}
}
}

View file

@ -0,0 +1,85 @@
package org.keycloak.testsuite.client.policies;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory;
import org.jboss.logging.Logger;
public class TrackEventsClientPolicyExecutor implements ClientPolicyExecutorProviderFactory, ClientPolicyExecutorProvider<TrackEventsClientPolicyExecutor.Configuration> {
public static final String PROVIDER_ID = "track-events-client-policy-executor";
private static final Logger log = Logger.getLogger(TrackEventsClientPolicyExecutor.class);
private static final TrackEventsClientPolicyExecutor SINGLETON = new TrackEventsClientPolicyExecutor();
private final List<ClientPolicyEvent> events;
public TrackEventsClientPolicyExecutor() {
this.events = new ArrayList<>();
}
@Override
public void executeOnEvent(ClientPolicyContext context) {
log.warnf("Executor with type: %s", context.getEvent());
events.add(context.getEvent());
}
public List<ClientPolicyEvent> getEvents() {
return events;
}
public void clearEventResult() {
events.clear();
}
@Override
public TrackEventsClientPolicyExecutor create(KeycloakSession session) {
return SINGLETON;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getProviderId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "Test Client Policy executor that checks if only specific policies with certain ClientPolicyEvent types are executed.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
}
}

View file

@ -0,0 +1 @@
org.keycloak.testsuite.client.policies.TrackEventsClientPolicyExecutor