From 16bb873a24ce5a614f986c7a77aa0e6ab49b3c6b Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Sun, 5 May 2024 01:28:50 +0200 Subject: [PATCH] Add example for publishing events via NATS --- .../local/dev/docker-compose-keycloakx.yml | 4 +- deployments/local/dev/docker-compose-nats.yml | 4 +- deployments/local/dev/nats/readme.md | 26 ++++++++ deployments/local/dev/nats/server.conf | 23 +++++++ keycloak/extensions/pom.xml | 36 ++++++++--- .../AcmeEventPublisherEventListener.java | 51 ++++++++++++++-- .../eventpublishing/EventPublisher.java | 14 +++++ .../eventpublishing/NatsEventPublisher.java | 60 +++++++++++++++---- pom.xml | 1 + 9 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 deployments/local/dev/nats/readme.md create mode 100644 deployments/local/dev/nats/server.conf create mode 100644 keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java diff --git a/deployments/local/dev/docker-compose-keycloakx.yml b/deployments/local/dev/docker-compose-keycloakx.yml index c7ca1289..264475ab 100644 --- a/deployments/local/dev/docker-compose-keycloakx.yml +++ b/deployments/local/dev/docker-compose-keycloakx.yml @@ -113,7 +113,9 @@ services: - ./run/keycloakx/logs:/opt/keycloak/logs:z - ./run/keycloakx/perf:/opt/keycloak/perf:z # Add keycloak extensions - - ../../../keycloak/extensions/target/extensions.jar:/opt/keycloak/providers/extensions.jar:z +# - ../../../keycloak/extensions/target/extensions.jar:/opt/keycloak/providers/extensions.jar:z + - ../../../keycloak/extensions/target/extensions-jar-with-dependencies.jar:/opt/keycloak/providers/extensions.jar:z + # Add third-party extensions # - ./keycloak-ext/keycloak-metrics-spi-3.0.0.jar:/opt/keycloak/providers/keycloak-metrics-spi.jar:z - ./keycloak-ext/keycloak-restrict-client-auth-23.0.0.jar:/opt/keycloak/providers/keycloak-restrict-client-auth.jar:z diff --git a/deployments/local/dev/docker-compose-nats.yml b/deployments/local/dev/docker-compose-nats.yml index b0754f86..0f68671a 100644 --- a/deployments/local/dev/docker-compose-nats.yml +++ b/deployments/local/dev/docker-compose-nats.yml @@ -4,4 +4,6 @@ services: ports: - "8222:8222" - "4222:4222" - hostname: acme-nats \ No newline at end of file + command: -c /etc/my-server.conf --name acme-nats -p 4222 + volumes: + - ./nats/server.conf:/etc/my-server.conf \ No newline at end of file diff --git a/deployments/local/dev/nats/readme.md b/deployments/local/dev/nats/readme.md new file mode 100644 index 00000000..a55723f0 --- /dev/null +++ b/deployments/local/dev/nats/readme.md @@ -0,0 +1,26 @@ +NATS Support +--- + +``` +nats context add localhost --description "Localhost" +``` + +Add username / password in context config +``` +vi ~/.config/nats/context/localhost.json +``` + +List contexts +``` +nats context ls +``` + +Select context +``` +nats ctx select localhost +``` + +Nats subscribe to keycloak subject +``` +nats sub acme.iam.keycloak> +``` \ No newline at end of file diff --git a/deployments/local/dev/nats/server.conf b/deployments/local/dev/nats/server.conf new file mode 100644 index 00000000..32f503c8 --- /dev/null +++ b/deployments/local/dev/nats/server.conf @@ -0,0 +1,23 @@ +accounts: { + $SYS: { + users: [ + { user: "admin", password: "password" } + ] + }, + KEYCLOAK: { + jetstream: enabled, + users: [ + { user: "keycloak", password: "keycloak" } + ] + } +} + +jetstream {} + +#cluster: { +# name: LOCAL, +# port: 6222, +# routes: [ +# "nats://acme_nats_1:6222" +# ] +#} \ No newline at end of file diff --git a/keycloak/extensions/pom.xml b/keycloak/extensions/pom.xml index 6b5d166c..3bc2c343 100644 --- a/keycloak/extensions/pom.xml +++ b/keycloak/extensions/pom.xml @@ -108,7 +108,7 @@ io.smallrye smallrye-health ${smallrye-health.version} - + provided @@ -122,7 +122,7 @@ org.eclipse.microprofile.health microprofile-health-api ${microprofile-health-api.version} - + provided @@ -132,6 +132,13 @@ provided + + io.nats + jnats + ${jnats.version} + + + org.keycloak @@ -151,11 +158,6 @@ - - io.nats - jnats - ${jnats.version} - org.jboss.resteasy @@ -293,7 +295,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.3.0 + ${maven-jar-plugin.version} @@ -302,6 +304,24 @@ + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + \ No newline at end of file diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java index d0ad5353..ca425220 100644 --- a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java @@ -2,6 +2,7 @@ import com.google.auto.service.AutoService; import lombok.RequiredArgsConstructor; +import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; @@ -9,7 +10,11 @@ import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; +import java.util.Map; + +@JBossLog @RequiredArgsConstructor public class AcmeEventPublisherEventListener implements EventListenerProvider { @@ -17,23 +22,35 @@ public class AcmeEventPublisherEventListener implements EventListenerProvider { private final KeycloakSession session; + private final EventPublisher publisher; + @Override public void onEvent(Event event) { - // NOOP + publisher.publish("acme.iam.keycloak.user", enrichUserEvent(event)); + } + + private Object enrichUserEvent(Event event) { + return event; } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { + publisher.publish("acme.iam.keycloak.admin", enrichAdminEvent(event, includeRepresentation)); + } + private Object enrichAdminEvent(AdminEvent event, boolean includeRepresentation) { + return event; } @Override public void close() { - + // NOOP } @AutoService(EventListenerProviderFactory.class) - public static class Factory implements EventListenerProviderFactory { + public static class Factory implements EventListenerProviderFactory, ServerInfoAwareProviderFactory { + + private EventPublisher publisher; @Override public String getId() { @@ -42,18 +59,42 @@ public String getId() { @Override // return singleton instance, create new AcmeAuditListener(session) or use lazy initialization public EventListenerProvider create(KeycloakSession session) { - return new AcmeEventPublisherEventListener(session); + return new AcmeEventPublisherEventListener(session, publisher); } @Override public void init(Config.Scope config) { /* configure factory */ + publisher = createNatsPublisher(config); + } + + private NatsEventPublisher createNatsPublisher(Config.Scope config) { + + String url = config.get("nats-url", "nats://acme-nats:4222"); + String username = config.get("nats-username", "keycloak"); + String password = config.get("nats-password", "keycloak"); + + var nats = new NatsEventPublisher(url, username, password); + nats.init(); + + log.info("Created new NatsPublisher"); + + return nats; } @Override // we could init our provider with information from other providers public void postInit(KeycloakSessionFactory factory) { /* post-process factory */ } @Override // close resources if necessary - public void close() { /* release resources if necessary */ } + public void close() { + if (publisher != null) { + publisher.close(); + } + } + + @Override + public Map getOperationalInfo() { + return publisher.getOperationalInfo(); + } } } diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java new file mode 100644 index 00000000..2db0f2af --- /dev/null +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java @@ -0,0 +1,14 @@ +package com.github.thomasdarimont.keycloak.custom.eventpublishing; + +import java.util.Map; + +public interface EventPublisher { + + void publish(String topic, Object event); + + Map getOperationalInfo(); + + void init(); + + void close(); +} diff --git a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java index 8e9d4165..5659a7f2 100644 --- a/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java +++ b/keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java @@ -3,27 +3,67 @@ import io.nats.client.Connection; import io.nats.client.Nats; import io.nats.client.Options; +import lombok.RequiredArgsConstructor; +import lombok.extern.jbosslog.JBossLog; import org.keycloak.util.JsonSerialization; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.util.Map; -public class NatsEventPublisher { +@JBossLog +@RequiredArgsConstructor +public class NatsEventPublisher implements EventPublisher { - public void publish(Object event) { + private final String url; - String natsURL = "nats://acme-nats:4222"; + private final String username; + + private final String password; + + private Connection connection; + + public void publish(String subject, Object event) { + try { + byte[] messageBytes = JsonSerialization.writeValueAsBytes(event); + connection.publish(subject, messageBytes); + } catch (IOException e) { + log.warn("Could not serialize event", e); + } + } + + public Map getOperationalInfo() { + return Map.of("url", url, "nats-username", username, "status", getStatus()); + } + + public String getStatus() { + if (connection == null) { + return null; + } + return connection.getStatus().name(); + } + + public void init() { Options options = Options.builder() // .connectionName("keycloak") // - .server(natsURL) // + .userInfo(username, password) // + .server(url) // .build(); - try (Connection nc = Nats.connect(options)) { - byte[] messageBytes = JsonSerialization.writeValueAsBytes(event); - nc.publish("acme.iam.keycloak.admin", messageBytes); - } catch (InterruptedException | IOException e) { - e.printStackTrace(); + try { + connection = Nats.connect(options); + } catch (Exception e) { + log.warn("Could not connect to nats server", e); + } + } + + public void close() { + if (connection != null) { + try { + connection.close(); + } catch (InterruptedException e) { + log.warn("Could not close connection", e); + } } } } diff --git a/pom.xml b/pom.xml index 962e7cf5..60ad3ee0 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ 0.43.4 3.2.5 3.2.5 + 3.4.0