diff --git a/README.md b/README.md index f76e200..26083fc 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Configuration values: | `backoffMaxInterval` | N | 60000 | Maximum back off time in milliseconds | | `backoffMultiplier` | N | 1.5 | Multiplier value (E.g. 1.5 is 50% increase per back off) | | `backoffRandomizationFactor` | N | 0.5 | Randomization factor (E.g. 0.5 results in a random period ranging between 50% below and 50% above the retry interval) | +| `cloudEventEnabled` | N | false | Generate a cloud event request to Http Server | ### Adding Configuration to your EventListenerProvider diff --git a/src/main/java/io/phasetwo/keycloak/events/HttpSenderEventListenerProvider.java b/src/main/java/io/phasetwo/keycloak/events/HttpSenderEventListenerProvider.java index 3673bff..933f99f 100644 --- a/src/main/java/io/phasetwo/keycloak/events/HttpSenderEventListenerProvider.java +++ b/src/main/java/io/phasetwo/keycloak/events/HttpSenderEventListenerProvider.java @@ -16,6 +16,8 @@ import org.apache.http.impl.client.HttpClients; import org.keycloak.broker.provider.util.LegacySimpleHttp; import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.idm.AdminEventRepresentation; +import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.util.JsonSerialization; @JBossLog @@ -30,6 +32,7 @@ public class HttpSenderEventListenerProvider extends SenderEventListenerProvider protected static final String BACKOFF_MAX_INTERVAL = "backoffMaxInterval"; protected static final String BACKOFF_MULTIPLIER = "backoffMultiplier"; protected static final String BACKOFF_RANDOMIZATION_FACTOR = "backoffRandomizationFactor"; + protected static final String CLOUD_EVENTS_ENABLED = "cloudEventEnabled"; public HttpSenderEventListenerProvider(KeycloakSession session, ScheduledExecutorService exec) { super(session, exec); @@ -54,6 +57,10 @@ String getTargetUri() { return config.get(TARGET_URI).toString(); } + Boolean getCloudEventsEnabled() { + return getBooleanOr(config, CLOUD_EVENTS_ENABLED, false); + } + Optional getSharedSecret() { return Optional.ofNullable(config.get(SHARED_SECRET)).map(Object::toString); } @@ -78,6 +85,32 @@ protected void send( request.header( "X-Keycloak-Signature", hmacFor(task.getEvent(), secret, algorithm.orElse(HMAC_SHA256_ALGORITHM)))); + + if (getCloudEventsEnabled()) { + Object eventObject = null; + eventObject = task.getEvent(); + if (eventObject instanceof EventRepresentation) { + EventRepresentation event = (EventRepresentation) eventObject; + request + .header("content-type", "application/json") + .header("ce-specversion", "1.0") + .header("ce-source", "keycloak") + .header("ce-type", event.getType()) + .header("ce-id", event.getRealmId() + "-" + event.getTime()) + .header("ce-partitionkey", event.getRealmId()); + + } else { + AdminEventRepresentation event = (AdminEventRepresentation) eventObject; + request + .header("content-type", "application/json") + .header("ce-specversion", "1.0") + .header("ce-source", "keycloak") + .header("ce-type", event.getOperationType() + "-" + event.getResourceType()) + .header("ce-id", event.getRealmId() + "-" + event.getTime()) + .header("ce-partitionkey", event.getRealmId()); + } + } + LegacySimpleHttp.Response response = request.asResponse(); int status = response.getStatus(); log.debugf("sent to %s (%d)", targetUri, status); diff --git a/src/main/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProvider.java b/src/main/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProvider.java index b704d1a..2825069 100644 --- a/src/main/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProvider.java +++ b/src/main/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProvider.java @@ -159,7 +159,8 @@ private ExtendedAdminEvent completeAdminEventAttributes(String uid, Event event) private ExtendedAdminEvent completeAdminEventAttributes(String uid, AdminEvent adminEvent) { RealmModel eventRealm = session.realms().getRealm(adminEvent.getRealmId()); RealmModel authRealm = session.realms().getRealm(adminEvent.getAuthDetails().getRealmId()); - ExtendedAdminEvent extendedAdminEvent = new ExtendedAdminEvent(uid, adminEvent, eventRealm, authRealm); + ExtendedAdminEvent extendedAdminEvent = + new ExtendedAdminEvent(uid, adminEvent, eventRealm, authRealm); // add always missing agent username ExtendedAuthDetails extendedAuthDetails = extendedAdminEvent.getAuthDetails(); if (!Strings.isNullOrEmpty(extendedAuthDetails.getUserId())) { diff --git a/src/main/java/io/phasetwo/keycloak/representation/ExtendedAdminEvent.java b/src/main/java/io/phasetwo/keycloak/representation/ExtendedAdminEvent.java index a2ffa8e..22a3e41 100644 --- a/src/main/java/io/phasetwo/keycloak/representation/ExtendedAdminEvent.java +++ b/src/main/java/io/phasetwo/keycloak/representation/ExtendedAdminEvent.java @@ -36,7 +36,8 @@ private static String createType(Event event) { public ExtendedAdminEvent() {} - public ExtendedAdminEvent(String uid, AdminEvent event, RealmModel eventRealm, RealmModel authRealm) { + public ExtendedAdminEvent( + String uid, AdminEvent event, RealmModel eventRealm, RealmModel authRealm) { this.uid = uid; this.type = createType(event); diff --git a/src/test/java/io/phasetwo/keycloak/Helpers.java b/src/test/java/io/phasetwo/keycloak/Helpers.java index 56c5e11..68f8d41 100644 --- a/src/test/java/io/phasetwo/keycloak/Helpers.java +++ b/src/test/java/io/phasetwo/keycloak/Helpers.java @@ -88,18 +88,15 @@ public static String createWebhook( } public static void removeWebhook( - Keycloak keycloak, - CloseableHttpClient httpClient, - String baseUrl, - String webhookId) - throws Exception { + Keycloak keycloak, CloseableHttpClient httpClient, String baseUrl, String webhookId) + throws Exception { - LegacySimpleHttp.Response response = - LegacySimpleHttp.doDelete(baseUrl + "/" + webhookId, httpClient) - .auth(keycloak.tokenManager().getAccessTokenString()) - .asResponse(); - assertThat(response.getStatus(), is(204)); - } + LegacySimpleHttp.Response response = + LegacySimpleHttp.doDelete(baseUrl + "/" + webhookId, httpClient) + .auth(keycloak.tokenManager().getAccessTokenString()) + .asResponse(); + assertThat(response.getStatus(), is(204)); + } public static String urlencode(String u) { try { diff --git a/src/test/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProviderTest.java b/src/test/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProviderTest.java index a653056..0fcc458 100644 --- a/src/test/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProviderTest.java +++ b/src/test/java/io/phasetwo/keycloak/events/WebhookSenderEventListenerProviderTest.java @@ -5,11 +5,18 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.*; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.xgp.http.server.Server; +import com.google.common.collect.ImmutableSet; +import io.phasetwo.keycloak.representation.ExtendedAdminEvent; +import io.phasetwo.keycloak.resources.AbstractResourceTest; +import jakarta.ws.rs.core.Response; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; - +import lombok.extern.jbosslog.JBossLog; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.junit.jupiter.api.Test; @@ -18,23 +25,13 @@ import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.ClientRepresentation; - import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.xgp.http.server.Server; -import com.google.common.collect.ImmutableSet; - -import io.phasetwo.keycloak.representation.ExtendedAdminEvent; -import io.phasetwo.keycloak.resources.AbstractResourceTest; -import jakarta.ws.rs.core.Response; -import lombok.extern.jbosslog.JBossLog; @JBossLog public class WebhookSenderEventListenerProviderTest extends AbstractResourceTest { - final static String TEST_REALM = "testRealm"; + static final String TEST_REALM = "testRealm"; CloseableHttpClient httpClient = HttpClients.createDefault(); String webhookUrl(String realm) { @@ -58,13 +55,14 @@ public void testAdminEventContainsCorrectRealmMasterMaster() throws Exception { AtomicReference body = new AtomicReference(); // create a server on a free port with a handler to listen for the event int port = WEBHOOK_SERVER_PORT; - String webhookId = createWebhook( - keycloak, - httpClient, - webhookUrl(REALM), - "http://host.testcontainers.internal:" + port + "/webhook", - "qlfwemke", - ImmutableSet.of("admin.*")); + String webhookId = + createWebhook( + keycloak, + httpClient, + webhookUrl(REALM), + "http://host.testcontainers.internal:" + port + "/webhook", + "qlfwemke", + ImmutableSet.of("admin.*")); Server server = new Server(port); server @@ -82,43 +80,39 @@ public void testAdminEventContainsCorrectRealmMasterMaster() throws Exception { Thread.sleep(1000l); try { - // cause an event to be sent - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setUsername("username"); - Response userResponse = realm.users().create(userRepresentation); - assertThat(userResponse.getStatus(), is(201)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - String receivedPayload = body.get(); - ExtendedAdminEvent event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); - assertThat(event.getType(), equalTo("admin.USER-CREATE")); - - // Now delete the user - List users = realm.users().search("username"); - assertThat(users.size(), is(1)); - String userId = users.getFirst().getId(); - userResponse = realm.users().delete(userId); - assertThat(userResponse.getStatus(), is(204)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - receivedPayload = body.get(); - event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); - assertThat(event.getType(), equalTo("admin.USER-DELETE")); - } - finally { - server.stop(); - removeWebhook(keycloak, - httpClient, - webhookUrl(REALM), - webhookId); + // cause an event to be sent + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername("username"); + Response userResponse = realm.users().create(userRepresentation); + assertThat(userResponse.getStatus(), is(201)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + String receivedPayload = body.get(); + ExtendedAdminEvent event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); + assertThat(event.getType(), equalTo("admin.USER-CREATE")); + + // Now delete the user + List users = realm.users().search("username"); + assertThat(users.size(), is(1)); + String userId = users.getFirst().getId(); + userResponse = realm.users().delete(userId); + assertThat(userResponse.getStatus(), is(204)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + receivedPayload = body.get(); + event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); + assertThat(event.getType(), equalTo("admin.USER-DELETE")); + } finally { + server.stop(); + removeWebhook(keycloak, httpClient, webhookUrl(REALM), webhookId); } } @@ -162,42 +156,41 @@ public void testAdminEventContainsCorrectRealmMasterTest() throws Exception { Thread.sleep(1000l); try { - // cause an event to be sent - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setUsername("username"); - Response userResponse = realm.users().create(userRepresentation); - assertThat(userResponse.getStatus(), is(201)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - String receivedPayload = body.get(); - ExtendedAdminEvent event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); - assertThat(event.getType(), equalTo("admin.USER-CREATE")); - - // Now delete the user - List users = realm.users().search("username"); - assertThat(users.size(), is(1)); - String userId = users.getFirst().getId(); - userResponse = realm.users().delete(userId); - assertThat(userResponse.getStatus(), is(204)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - receivedPayload = body.get(); - event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); - assertThat(event.getType(), equalTo("admin.USER-DELETE")); - - // cleanup - realm.remove(); - } - finally { - server.stop(); + // cause an event to be sent + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername("username"); + Response userResponse = realm.users().create(userRepresentation); + assertThat(userResponse.getStatus(), is(201)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + String receivedPayload = body.get(); + ExtendedAdminEvent event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); + assertThat(event.getType(), equalTo("admin.USER-CREATE")); + + // Now delete the user + List users = realm.users().search("username"); + assertThat(users.size(), is(1)); + String userId = users.getFirst().getId(); + userResponse = realm.users().delete(userId); + assertThat(userResponse.getStatus(), is(204)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + receivedPayload = body.get(); + event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); + assertThat(event.getType(), equalTo("admin.USER-DELETE")); + + // cleanup + realm.remove(); + } finally { + server.stop(); } } @@ -232,16 +225,11 @@ public void testAdminEventContainsCorrectRealmTestTest() throws Exception { String realmManagementId = realm.clients().findByClientId("realm-management").get(0).getId(); String clientId = realm.clients().findByClientId(realmAdminClientId).get(0).getId(); String serviceUserId = realm.clients().get(clientId).getServiceAccountUser().getId(); - List availableRoles = realm - .users() - .get(serviceUserId) - .roles() - .clientLevel(realmManagementId) - .listAvailable(); - List rolesToAssign = availableRoles - .stream() - .filter(r -> - "realm-admin".equalsIgnoreCase(r.getName())) + List availableRoles = + realm.users().get(serviceUserId).roles().clientLevel(realmManagementId).listAvailable(); + List rolesToAssign = + availableRoles.stream() + .filter(r -> "realm-admin".equalsIgnoreCase(r.getName())) .collect(Collectors.toList()); assertThat(rolesToAssign.size(), is(1)); realm.users().get(serviceUserId).roles().clientLevel(realmManagementId).add(rolesToAssign); @@ -273,183 +261,176 @@ public void testAdminEventContainsCorrectRealmTestTest() throws Exception { Thread.sleep(1000l); try { - // log in to the test realm - Keycloak keycloakTestRealm = KeycloakBuilder.builder() - .serverUrl(getAuthUrl()) - .clientId(realmAdminClientId) - .clientSecret(realmAdminClientSecret) - .realm(TEST_REALM) - .grantType(OAuth2Constants.CLIENT_CREDENTIALS) - .build(); - realm = keycloakTestRealm.realm(TEST_REALM); - - // cause an event to be sent - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setUsername("username"); - Response userResponse = realm.users().create(userRepresentation); - assertThat(userResponse.getStatus(), is(201)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - String receivedPayload = body.get(); - ExtendedAdminEvent event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getType(), equalTo("admin.USER-CREATE")); - - // Now delete the user - List users = realm.users().search("username"); - assertThat(users.size(), is(1)); - String userId = users.getFirst().getId(); - userResponse = realm.users().delete(userId); - assertThat(userResponse.getStatus(), is(204)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - receivedPayload = body.get(); - event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getType(), equalTo("admin.USER-DELETE")); - - // cleanup - realm = keycloak.realm(TEST_REALM); - realm.remove(); - } - finally { - server.stop(); + // log in to the test realm + Keycloak keycloakTestRealm = + KeycloakBuilder.builder() + .serverUrl(getAuthUrl()) + .clientId(realmAdminClientId) + .clientSecret(realmAdminClientSecret) + .realm(TEST_REALM) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .build(); + realm = keycloakTestRealm.realm(TEST_REALM); + + // cause an event to be sent + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername("username"); + Response userResponse = realm.users().create(userRepresentation); + assertThat(userResponse.getStatus(), is(201)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + String receivedPayload = body.get(); + ExtendedAdminEvent event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getType(), equalTo("admin.USER-CREATE")); + + // Now delete the user + List users = realm.users().search("username"); + assertThat(users.size(), is(1)); + String userId = users.getFirst().getId(); + userResponse = realm.users().delete(userId); + assertThat(userResponse.getStatus(), is(204)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + receivedPayload = body.get(); + event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getType(), equalTo("admin.USER-DELETE")); + + // cleanup + realm = keycloak.realm(TEST_REALM); + realm.remove(); + } finally { + server.stop(); } } @Test - public void testAdminEventContainsCorrectRealmCreatedByMaterRemovedByTest() throws Exception - { - // Create a realm for tests - RealmRepresentation realmRepresentation = new RealmRepresentation(); - realmRepresentation.setId(TEST_REALM); - realmRepresentation.setDisplayName(TEST_REALM); - realmRepresentation.setAdminEventsEnabled(true); - realmRepresentation.setAdminEventsDetailsEnabled(true); - realmRepresentation.setRealm(TEST_REALM); - realmRepresentation.setEventsListeners(Arrays.asList("ext-event-webhook")); - realmRepresentation.setEnabled(true); - keycloak.realms().create(realmRepresentation); - RealmResource realm = keycloak.realm(TEST_REALM); - - // Creating admin client - final String realmAdminClientId = "realmAdmin"; - final String realmAdminClientSecret = "realmPassword"; - ClientRepresentation client = new ClientRepresentation(); - client.setSecret(realmAdminClientSecret); - client.setClientId(realmAdminClientId); - client.setEnabled(true); - client.setServiceAccountsEnabled(true); - client.setPublicClient(false); - client.setProtocol("openid-connect"); - Response clientCreationResponse = realm.clients().create(client); - assertThat(clientCreationResponse.getStatus(), is(201)); - - // Adding rights - String realmManagementId = realm.clients().findByClientId("realm-management").get(0).getId(); - String clientId = realm.clients().findByClientId(realmAdminClientId).get(0).getId(); - String serviceUserId = realm.clients().get(clientId).getServiceAccountUser().getId(); - List availableRoles = realm - .users() - .get(serviceUserId) - .roles() - .clientLevel(realmManagementId) - .listAvailable(); - List rolesToAssign = availableRoles - .stream() - .filter(r -> - "realm-admin".equalsIgnoreCase(r.getName())) - .collect(Collectors.toList()); - assertThat(rolesToAssign.size(), is(1)); - realm.users().get(serviceUserId).roles().clientLevel(realmManagementId).add(rolesToAssign); - - AtomicReference body = new AtomicReference(); - // create a server on a free port with a handler to listen for the event - int port = WEBHOOK_SERVER_PORT; - createWebhook( - keycloak, - httpClient, - webhookUrl(TEST_REALM), - "http://host.testcontainers.internal:" + port + "/webhook", - "qlfwemke", - ImmutableSet.of("admin.*")); - - Server server = new Server(port); - server - .router() - .POST( - "/webhook", - (request, response) -> { - String r = request.body(); - log.infof("%s", r); - body.set(r); - response.body("OK"); - response.status(202); - }); - server.start(); - Thread.sleep(1000l); - - try { - // cause an event to be sent - UserRepresentation userRepresentation = new UserRepresentation(); - userRepresentation.setUsername("username"); - Response userResponse = realm.users().create(userRepresentation); - assertThat(userResponse.getStatus(), is(201)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - String receivedPayload = body.get(); - ExtendedAdminEvent event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); - assertThat(event.getType(), equalTo("admin.USER-CREATE")); - - // log in to the test realm - Keycloak keycloakTestRealm = KeycloakBuilder.builder() - .serverUrl(getAuthUrl()) - .clientId(realmAdminClientId) - .clientSecret(realmAdminClientSecret) - .realm(TEST_REALM) - .grantType(OAuth2Constants.CLIENT_CREDENTIALS) - .build(); - realm = keycloakTestRealm.realm(TEST_REALM); - - // Now delete the user - List users = realm.users().search("username"); - assertThat(users.size(), is(1)); - String userId = users.getFirst().getId(); - userResponse = realm.users().delete(userId); - assertThat(userResponse.getStatus(), is(204)); - - Thread.sleep(1000l); - - // check the handler for the event, after a delay - receivedPayload = body.get(); - event = parseEvent(receivedPayload); - assertThat(event.getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); - assertThat(event.getType(), equalTo("admin.USER-DELETE")); - - // cleanup - realm = keycloak.realm(TEST_REALM); - realm.remove(); - } - finally { - server.stop(); - } + public void testAdminEventContainsCorrectRealmCreatedByMaterRemovedByTest() throws Exception { + // Create a realm for tests + RealmRepresentation realmRepresentation = new RealmRepresentation(); + realmRepresentation.setId(TEST_REALM); + realmRepresentation.setDisplayName(TEST_REALM); + realmRepresentation.setAdminEventsEnabled(true); + realmRepresentation.setAdminEventsDetailsEnabled(true); + realmRepresentation.setRealm(TEST_REALM); + realmRepresentation.setEventsListeners(Arrays.asList("ext-event-webhook")); + realmRepresentation.setEnabled(true); + keycloak.realms().create(realmRepresentation); + RealmResource realm = keycloak.realm(TEST_REALM); + + // Creating admin client + final String realmAdminClientId = "realmAdmin"; + final String realmAdminClientSecret = "realmPassword"; + ClientRepresentation client = new ClientRepresentation(); + client.setSecret(realmAdminClientSecret); + client.setClientId(realmAdminClientId); + client.setEnabled(true); + client.setServiceAccountsEnabled(true); + client.setPublicClient(false); + client.setProtocol("openid-connect"); + Response clientCreationResponse = realm.clients().create(client); + assertThat(clientCreationResponse.getStatus(), is(201)); + + // Adding rights + String realmManagementId = realm.clients().findByClientId("realm-management").get(0).getId(); + String clientId = realm.clients().findByClientId(realmAdminClientId).get(0).getId(); + String serviceUserId = realm.clients().get(clientId).getServiceAccountUser().getId(); + List availableRoles = + realm.users().get(serviceUserId).roles().clientLevel(realmManagementId).listAvailable(); + List rolesToAssign = + availableRoles.stream() + .filter(r -> "realm-admin".equalsIgnoreCase(r.getName())) + .collect(Collectors.toList()); + assertThat(rolesToAssign.size(), is(1)); + realm.users().get(serviceUserId).roles().clientLevel(realmManagementId).add(rolesToAssign); + + AtomicReference body = new AtomicReference(); + // create a server on a free port with a handler to listen for the event + int port = WEBHOOK_SERVER_PORT; + createWebhook( + keycloak, + httpClient, + webhookUrl(TEST_REALM), + "http://host.testcontainers.internal:" + port + "/webhook", + "qlfwemke", + ImmutableSet.of("admin.*")); + + Server server = new Server(port); + server + .router() + .POST( + "/webhook", + (request, response) -> { + String r = request.body(); + log.infof("%s", r); + body.set(r); + response.body("OK"); + response.status(202); + }); + server.start(); + Thread.sleep(1000l); + + try { + // cause an event to be sent + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername("username"); + Response userResponse = realm.users().create(userRepresentation); + assertThat(userResponse.getStatus(), is(201)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + String receivedPayload = body.get(); + ExtendedAdminEvent event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(REALM)); + assertThat(event.getType(), equalTo("admin.USER-CREATE")); + + // log in to the test realm + Keycloak keycloakTestRealm = + KeycloakBuilder.builder() + .serverUrl(getAuthUrl()) + .clientId(realmAdminClientId) + .clientSecret(realmAdminClientSecret) + .realm(TEST_REALM) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .build(); + realm = keycloakTestRealm.realm(TEST_REALM); + + // Now delete the user + List users = realm.users().search("username"); + assertThat(users.size(), is(1)); + String userId = users.getFirst().getId(); + userResponse = realm.users().delete(userId); + assertThat(userResponse.getStatus(), is(204)); + + Thread.sleep(1000l); + + // check the handler for the event, after a delay + receivedPayload = body.get(); + event = parseEvent(receivedPayload); + assertThat(event.getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getAuthDetails().getRealmId(), equalTo(TEST_REALM)); + assertThat(event.getType(), equalTo("admin.USER-DELETE")); + + // cleanup + realm = keycloak.realm(TEST_REALM); + realm.remove(); + } finally { + server.stop(); + } } - private static ExtendedAdminEvent parseEvent(String input) throws Exception - { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - return mapper.readValue(input, ExtendedAdminEvent.class); + private static ExtendedAdminEvent parseEvent(String input) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return mapper.readValue(input, ExtendedAdminEvent.class); } }