diff --git a/.gitignore b/.gitignore index b139121..764fa81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Just so that we don't store the docker image build output into the repository. -mvn-output/ \ No newline at end of file +mvn-output/ +target/classes/**/*.class diff --git a/README.md b/README.md index e370680..b2c9d02 100755 --- a/README.md +++ b/README.md @@ -33,6 +33,18 @@ sudo chown $USER:$USER mvn-output # Deploy * Copy target/event-listener-http-jar-with-dependencies.jar to {KEYCLOAK_HOME}/standalone/deployments + +# Configuration + +## Option 1 +* Configure the following env variables : + + - HTTP_EVENT_SERVERURI - default: http://127.0.0.1:8080/webhook + - HTTP_EVENT_USERNAME - default: keycloak + - HTTP_EVENT_PASSWORD - default: keycloak + +* Restart the keycloak server. +## Option 2 * Edit standalone.xml to configure the Webhook settings. Find the following section in the configuration: @@ -50,17 +62,16 @@ And add below: - ``` + Leave username and password out if the service allows anonymous access. -If unset, the default message topic is "keycloak/events". * Restart the keycloak server. -# Use +# Usage Add/Update a user, your webhook should be called, looks at the keycloak syslog for debug Request example diff --git a/pom.xml b/pom.xml index 8d01d3e..294f75f 100755 --- a/pom.xml +++ b/pom.xml @@ -1,24 +1,7 @@ - - org.softwarefactory.keycloak.providers.events.http - 5.0.0 + 13.0.0 Keycloak: Event Publisher to HTTP @@ -32,35 +15,16 @@ ${project.version} 1.2.2.Final - 1.0.2.Final - 1.0.1.Final 7.4.Final 2.6 1.4.1 - 2.19.1 - 1.6.0 - 1.8 - 1.4 3.0.2 3.1 - 2.3.2 - 1.2.1.Final - 2.0.2.Final - 8.2.1.Final - 4.12 - 1.3 - 1.6.1 2.9.5 - true ./jboss-cli.sh 10090 - 3.11.0 - 1.4.0.Final - 2.5.1 - 1.1.2 - 1.0.1 @@ -82,40 +46,10 @@ provided - org.jboss.arquillian.protocol - arquillian-protocol-servlet - 1.4.1.Final - test - - - org.jboss.arquillian.graphene - graphene-webdriver - ${arquillian-graphene.version} - pom - test - - - org.jboss.arquillian.extension - arquillian-phantom-driver - ${arquillian-phantom.version} - test - - - org.wildfly.extras.creaper - creaper-core - ${version.creaper} - test - - - com.google.guava - guava - - - - - org.hamcrest - hamcrest-all - 1.3 + org.keycloak + keycloak-services + provided + ${version.keycloak} com.squareup.okhttp3 @@ -137,17 +71,6 @@ 1.8 - - org.apache.maven.plugins - maven-surefire-plugin - ${version.surefire.plugin} - - - ${keycloak.management.port} - ${project.build.directory} - - - org.apache.maven.plugins diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/AdminEventNotification.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/AdminEventNotification.java new file mode 100644 index 0000000..e16d766 --- /dev/null +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/AdminEventNotification.java @@ -0,0 +1,31 @@ +package org.softwarefactory.keycloak.providers.events.http; + +import java.io.Serializable; + +import org.keycloak.events.admin.AdminEvent; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = Id.CLASS) +public class AdminEventNotification extends AdminEvent implements Serializable { + + private static final long serialVersionUID = -7367949289101799624L; + + public static AdminEventNotification create(AdminEvent adminEvent) { + AdminEventNotification msg = new AdminEventNotification(); + msg.setAuthDetails(adminEvent.getAuthDetails()); + msg.setError(adminEvent.getError()); + msg.setOperationType(adminEvent.getOperationType()); + msg.setRealmId(adminEvent.getRealmId()); + msg.setRepresentation(adminEvent.getRepresentation()); + msg.setResourcePath(adminEvent.getResourcePath()); + msg.setResourceType(adminEvent.getResourceType()); + msg.setTime(adminEvent.getTime()); + return msg; + } + + +} diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/ClientEventNotification.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/ClientEventNotification.java new file mode 100644 index 0000000..b236cb7 --- /dev/null +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/ClientEventNotification.java @@ -0,0 +1,33 @@ +package org.softwarefactory.keycloak.providers.events.http; + +import java.io.Serializable; + +import org.keycloak.events.Event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = Id.CLASS) +public class ClientEventNotification extends Event implements Serializable { + + private static final long serialVersionUID = -2192461924304841222L; + + public static ClientEventNotification create(Event event) { + ClientEventNotification msg = new ClientEventNotification(); + msg.setClientId(event.getClientId()); + msg.setDetails(event.getDetails()); + msg.setError(event.getError()); + msg.setIpAddress(event.getIpAddress()); + msg.setRealmId(event.getRealmId()); + msg.setSessionId(event.getSessionId()); + msg.setTime(event.getTime()); + msg.setType(event.getType()); + msg.setUserId(event.getUserId()); + + return msg; + } + + +} \ No newline at end of file diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventConfiguration.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventConfiguration.java new file mode 100644 index 0000000..27ba024 --- /dev/null +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.softwarefactory.keycloak.providers.events.http; + + +import org.keycloak.Config.Scope; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Abdoulaye Traore + */ +public class HTTPEventConfiguration { + + private String serverUri; + private String username; + private String password; + + public static final ObjectMapper httpEventConfigurationObjectMapper = new ObjectMapper(); + + public static HTTPEventConfiguration createFromScope(Scope config) { + HTTPEventConfiguration configuration = new HTTPEventConfiguration(); + + configuration.serverUri = resolveConfigVar(config, "serverUri", "http://127.0.0.1:8080/webhook"); + configuration.username = resolveConfigVar(config, "username", "keycloak"); + configuration.password = resolveConfigVar(config, "password", "keycloak"); + + return configuration; + + } + + private static String resolveConfigVar(Scope config, String variableName, String defaultValue) { + + String value = defaultValue; + if(config != null && config.get(variableName) != null) { + value = config.get(variableName); + } else { + //try from env variables eg: HTTP_EVENT_: + String envVariableName = "HTTP_EVENT_" + variableName.toUpperCase(); + if(System.getenv(envVariableName) != null) { + value = System.getenv(envVariableName); + } + } + System.out.println("HTTPEventListener configuration: " + variableName + "=" + value); + return value; + + } + + public static String writeAsJson(Object object, boolean isPretty) { + String messageAsJson = "unparsable"; + try { + if(isPretty) { + messageAsJson = HTTPEventConfiguration.httpEventConfigurationObjectMapper + .writerWithDefaultPrettyPrinter().writeValueAsString(object); + } else { + messageAsJson = HTTPEventConfiguration.httpEventConfigurationObjectMapper + .writeValueAsString(object); + } + + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + return messageAsJson; + } + + + + public String getServerUri() { + return serverUri; + } + public void setServerUri(String serverUri) { + this.serverUri = serverUri; + } + + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + + +} diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java index 9b45e36..cfebc62 100755 --- a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java @@ -17,41 +17,48 @@ package org.softwarefactory.keycloak.providers.events.http; +import java.io.IOException; +import java.util.Set; + import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerTransaction; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.OperationType; +import org.keycloak.models.KeycloakSession; -import java.util.Map; -import java.util.Set; -import java.lang.Exception; - -import okhttp3.*; -import okhttp3.OkHttpClient.Builder; - -import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; /** - * @author Jessy Lenne + * @author Abdoulaye Traore */ public class HTTPEventListenerProvider implements EventListenerProvider { + private final OkHttpClient httpClient = new OkHttpClient(); private Set excludedEvents; private Set excludedAdminOperations; private String serverUri; private String username; private String password; - public static final String publisherId = "keycloak"; - public String TOPIC; - public HTTPEventListenerProvider(Set excludedEvents, Set excludedAdminOperations, String serverUri, String username, String password, String topic) { + private KeycloakSession session; + + private EventListenerTransaction tx = new EventListenerTransaction(this::publishAdminEvent, this::publishEvent); + + public HTTPEventListenerProvider(Set excludedEvents, Set excludedAdminOperations, String serverUri, String username, String password, KeycloakSession session) { this.excludedEvents = excludedEvents; this.excludedAdminOperations = excludedAdminOperations; this.serverUri = serverUri; this.username = username; this.password = password; - this.TOPIC = topic; + + this.session = session; + this.session.getTransactionManager().enlistAfterCompletion(tx); } @Override @@ -60,145 +67,66 @@ public void onEvent(Event event) { if (excludedEvents != null && excludedEvents.contains(event.getType())) { return; } else { - String stringEvent = toString(event); - try { - RequestBody formBody = new FormBody.Builder() - .add("json", stringEvent) - .build(); - - okhttp3.Request.Builder builder = new Request.Builder() - .url(this.serverUri) - .addHeader("User-Agent", "KeycloakHttp Bot"); - - - if (this.username != null && this.password != null) { - builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); - } - - Request request = builder.post(formBody) - .build(); - - Response response = httpClient.newCall(request).execute(); - - if (!response.isSuccessful()) { - throw new IOException("Unexpected code " + response); - } - - // Get response body - System.out.println(response.body().string()); - } catch(Exception e) { - // ? - System.out.println("UH OH!! " + e.toString()); - e.printStackTrace(); - return; - } + tx.addEvent(event); } } @Override - public void onEvent(AdminEvent event, boolean includeRepresentation) { + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { // Ignore excluded operations - if (excludedAdminOperations != null && excludedAdminOperations.contains(event.getOperationType())) { + if (excludedAdminOperations != null && excludedAdminOperations.contains(adminEvent.getOperationType())) { return; } else { - String stringEvent = toString(event); - try { - RequestBody formBody = new FormBody.Builder() - .add("json", stringEvent) - .build(); - - okhttp3.Request.Builder builder = new Request.Builder() - .url(this.serverUri) - .addHeader("User-Agent", "KeycloakHttp Bot"); - - - if (this.username != null && this.password != null) { - builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); - } - - Request request = builder.post(formBody) - .build(); - - Response response = httpClient.newCall(request).execute(); - - if (!response.isSuccessful()) { - throw new IOException("Unexpected code " + response); - } - - // Get response body - System.out.println(response.body().string()); - } catch(Exception e) { - // ? - System.out.println("UH OH!! " + e.toString()); - e.printStackTrace(); - return; - } + tx.addAdminEvent(adminEvent, includeRepresentation); } } + public void publishEvent(Event event) { + ClientEventNotification notification = ClientEventNotification.create(event); + String notificationAsString = HTTPEventConfiguration.writeAsJson(notification, false); + this.sendEvent(notificationAsString); + } - private String toString(Event event) { - StringBuilder sb = new StringBuilder(); - - sb.append("{'type': '"); - sb.append(event.getType()); - sb.append("', 'realmId': '"); - sb.append(event.getRealmId()); - sb.append("', 'clientId': '"); - sb.append(event.getClientId()); - sb.append("', 'userId': '"); - sb.append(event.getUserId()); - sb.append("', 'ipAddress': '"); - sb.append(event.getIpAddress()); - sb.append("'"); - - if (event.getError() != null) { - sb.append(", 'error': '"); - sb.append(event.getError()); - sb.append("'"); - } - sb.append(", 'details': {"); - if (event.getDetails() != null) { - for (Map.Entry e : event.getDetails().entrySet()) { - sb.append("'"); - sb.append(e.getKey()); - sb.append("': '"); - sb.append(e.getValue()); - sb.append("', "); + public void publishAdminEvent(AdminEvent event, boolean includeRepresentation) { + AdminEventNotification notification = AdminEventNotification.create(event); + String notificationAsString = HTTPEventConfiguration.writeAsJson(notification, false); + this.sendEvent(notificationAsString); + } + + private void sendEvent(String event) { + try { + MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + RequestBody formBody = RequestBody.create(event, JSON); + + okhttp3.Request.Builder builder = new Request.Builder() + .url(this.serverUri) + .addHeader("User-Agent", "KeycloakHttp Bot"); + + + if (this.username != null && this.password != null) { + builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); + } + + Request request = builder.post(formBody) + .build(); + + Response response = httpClient.newCall(request).execute(); + + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); } - sb.append("}}"); - } - return sb.toString(); - } - - - private String toString(AdminEvent adminEvent) { - StringBuilder sb = new StringBuilder(); - - sb.append("{'type': '"); - sb.append(adminEvent.getOperationType()); - sb.append("', 'realmId': '"); - sb.append(adminEvent.getAuthDetails().getRealmId()); - sb.append("', 'clientId': '"); - sb.append(adminEvent.getAuthDetails().getClientId()); - sb.append("', 'userId': '"); - sb.append(adminEvent.getAuthDetails().getUserId()); - sb.append("', 'ipAddress': '"); - sb.append(adminEvent.getAuthDetails().getIpAddress()); - sb.append("', 'resourcePath': '"); - sb.append(adminEvent.getResourcePath()); - sb.append("'"); - - if (adminEvent.getError() != null) { - sb.append(", 'error': '"); - sb.append(adminEvent.getError()); - sb.append("'"); + // Get response body + System.out.println(response.body().string()); + } catch(Exception e) { + System.out.println("An error occured while sending event : " + e.toString()); + e.printStackTrace(); + return; } - sb.append("}"); - return sb.toString(); } + @Override public void close() { } diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java index 101a22d..8065f1a 100755 --- a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java @@ -27,10 +27,9 @@ import java.util.HashSet; import java.util.Set; -import java.lang.Exception; /** - * @author Jessy Lennee + * @author Abdoulaye Traore */ public class HTTPEventListenerProviderFactory implements EventListenerProviderFactory { @@ -39,11 +38,10 @@ public class HTTPEventListenerProviderFactory implements EventListenerProviderFa private String serverUri; private String username; private String password; - private String topic; @Override public EventListenerProvider create(KeycloakSession session) { - return new HTTPEventListenerProvider(excludedEvents, excludedAdminOperations, serverUri, username, password, topic); + return new HTTPEventListenerProvider(excludedEvents, excludedAdminOperations, serverUri, username, password, session); } @Override @@ -64,16 +62,19 @@ public void init(Config.Scope config) { } } - serverUri = config.get("serverUri", "http://nginx/frontend_dev.php/webhook/keycloak"); - username = config.get("username", null); - password = config.get("password", null); - topic = config.get("topic", "keycloak/events"); + HTTPEventConfiguration configuration = HTTPEventConfiguration.createFromScope(config); + + + serverUri = configuration.getServerUri(); + username = configuration.getUsername(); + password = configuration.getPassword(); } @Override public void postInit(KeycloakSessionFactory factory) { } + @Override public void close() { } diff --git a/target/classes/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/target/classes/META-INF/services/org.keycloak.events.EventListenerProviderFactory deleted file mode 100644 index cb8a9ef..0000000 --- a/target/classes/META-INF/services/org.keycloak.events.EventListenerProviderFactory +++ /dev/null @@ -1,19 +0,0 @@ - -# -# Copyright 2019 Red Hat, Inc. and/or its affiliates -# and other contributors as indicated by the @author tags. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -org.softwarefactory.keycloak.providers.events.http.HTTPEventListenerProviderFactory diff --git a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class deleted file mode 100644 index 5627ff0..0000000 Binary files a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class and /dev/null differ diff --git a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class deleted file mode 100644 index fd62952..0000000 Binary files a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class and /dev/null differ diff --git a/target/event-listener-http-jar-with-dependencies.jar b/target/event-listener-http-jar-with-dependencies.jar deleted file mode 100644 index 67223ce..0000000 Binary files a/target/event-listener-http-jar-with-dependencies.jar and /dev/null differ diff --git a/target/event-listener-http.jar b/target/event-listener-http.jar deleted file mode 100644 index afc334f..0000000 Binary files a/target/event-listener-http.jar and /dev/null differ diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties deleted file mode 100644 index 33aa8ac..0000000 --- a/target/maven-archiver/pom.properties +++ /dev/null @@ -1,5 +0,0 @@ -#Generated by Maven -#Tue Jan 28 10:33:34 CET 2020 -groupId=org.softwarefactory.keycloak.providers.events.http -artifactId=event-listener-http -version=5.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst deleted file mode 100644 index bf15e25..0000000 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ /dev/null @@ -1,2 +0,0 @@ -org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class -org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst deleted file mode 100644 index 8223f33..0000000 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ /dev/null @@ -1,2 +0,0 @@ -/Users/jessylenne/Documents/www/keycloak-event-listener-http/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java -/Users/jessylenne/Documents/www/keycloak-event-listener-http/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java