From 86860cb96901134e5b2a9ed84f1ebc64dc0b10a8 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Wed, 11 Dec 2024 12:02:46 +0100 Subject: [PATCH] Attempt to replicate the Keycloak implementation --- .vscode/settings.json | 2 +- pom.xml | 44 ++-- .../logger/AdminEventListenerProvider.java | 212 ++++++++++++++++-- .../AdminEventListenerProviderFactory.java | 118 +++++++--- 4 files changed, 311 insertions(+), 65 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c5f3f6b..e0f15db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6ab4eb5..b46e61a 100644 --- a/pom.xml +++ b/pom.xml @@ -11,38 +11,36 @@ 17 + 26.0.7 - + - org.keycloak - keycloak-core - 26.0.0 + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided - org.keycloak - keycloak-server-spi - 26.0.0 - - - org.keycloak - keycloak-server-spi-private - 26.0.0 + org.keycloak + keycloak-services + ${keycloak.version} + provided - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 11 - 11 - - - + + + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + false + + + \ No newline at end of file diff --git a/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProvider.java b/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProvider.java index 972680d..3530cd8 100644 --- a/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProvider.java +++ b/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProvider.java @@ -1,18 +1,198 @@ package org.keycloakify.keycloak.event.logger; -import org.keycloak.events.Event; -import org.keycloak.events.EventListenerProvider; -import org.keycloak.events.admin.AdminEvent; - -public class AdminEventListenerProvider implements EventListenerProvider { - - public void close() { - } - - public void onEvent(Event event) { - } - - public void onEvent(AdminEvent event, boolean includeRepresentation) { - System.out.println("UPDATE: " + event.getOperationType().toString()); - } -} \ No newline at end of file + import org.jboss.logging.Logger; + import org.keycloak.common.util.StackUtil; + import org.keycloak.events.Event; + import org.keycloak.events.EventListenerProvider; + import org.keycloak.events.EventListenerTransaction; + import org.keycloak.events.admin.AdminEvent; + import org.keycloak.models.KeycloakContext; + import org.keycloak.models.KeycloakSession; + import org.keycloak.sessions.AuthenticationSessionModel; + import org.keycloak.utils.StringUtil; + + import jakarta.ws.rs.core.Cookie; + import jakarta.ws.rs.core.HttpHeaders; + import jakarta.ws.rs.core.UriInfo; + import java.util.Map; + + /** + * @author Stian Thorgersen + */ + public class AdminEventListenerProvider implements EventListenerProvider { + + private final KeycloakSession session; + private final Logger logger; + private final Logger.Level successLevel; + private final Logger.Level errorLevel; + private final boolean sanitize; + private final Character quotes; + private final EventListenerTransaction tx = new EventListenerTransaction(this::logAdminEvent, this::logEvent); + + public AdminEventListenerProvider(KeycloakSession session, Logger logger, + Logger.Level successLevel, Logger.Level errorLevel, Character quotes, boolean sanitize) { + this.session = session; + this.logger = logger; + this.successLevel = successLevel; + this.errorLevel = errorLevel; + this.sanitize = sanitize; + this.quotes = quotes; + this.session.getTransactionManager().enlistAfterCompletion(tx); + } + + @Override + public void onEvent(Event event) { + tx.addEvent(event); + } + + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + tx.addAdminEvent(adminEvent, includeRepresentation); + } + + private void sanitize(StringBuilder sb, String str) { + if (quotes != null) { + sb.append(quotes); + } + if (sanitize) { + str = StringUtil.sanitizeSpacesAndQuotes(str, quotes); + } + sb.append(str); + if (quotes != null) { + sb.append(quotes); + } + } + + private void logEvent(Event event) { + Logger.Level level = event.getError() != null ? errorLevel : successLevel; + + if (logger.isEnabled(level)) { + StringBuilder sb = new StringBuilder(); + + sb.append("type="); + sanitize(sb, event.getType().toString()); + sb.append(", realmId="); + sanitize(sb, event.getRealmId()); + sb.append(", realmName="); + sanitize(sb, event.getRealmName()); + sb.append(", clientId="); + sanitize(sb, event.getClientId()); + sb.append(", userId="); + sanitize(sb, event.getUserId()); + if (event.getSessionId() != null) { + sb.append(", sessionId="); + sanitize(sb, event.getSessionId()); + } + sb.append(", ipAddress="); + sanitize(sb, event.getIpAddress()); + + if (event.getError() != null) { + sb.append(", error="); + sanitize(sb, event.getError()); + } + + if (event.getDetails() != null) { + for (Map.Entry e : event.getDetails().entrySet()) { + sb.append(", "); + sb.append(StringUtil.sanitizeSpacesAndQuotes(e.getKey(), null)); + sb.append("="); + sanitize(sb, e.getValue()); + } + } + + AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); + if(authSession!=null) { + sb.append(", authSessionParentId="); + sanitize(sb, authSession.getParentSession().getId()); + sb.append(", authSessionTabId="); + sanitize(sb, authSession.getTabId()); + } + + if(logger.isTraceEnabled()) { + setKeycloakContext(sb); + + if (StackUtil.isShortStackTraceEnabled()) { + sb.append(", stackTrace=").append(StackUtil.getShortStackTrace()); + } + } + + logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString()); + } + } + + private void logAdminEvent(AdminEvent adminEvent, boolean includeRepresentation) { + Logger.Level level = adminEvent.getError() != null ? errorLevel : successLevel; + + if (logger.isEnabled(level)) { + StringBuilder sb = new StringBuilder(); + + sb.append("operationType="); + sanitize(sb, adminEvent.getOperationType().toString()); + sb.append(", realmId="); + sanitize(sb, adminEvent.getAuthDetails().getRealmId()); + sb.append(", realmName="); + sanitize(sb, adminEvent.getAuthDetails().getRealmName()); + sb.append(", clientId="); + sanitize(sb, adminEvent.getAuthDetails().getClientId()); + sb.append(", userId="); + sanitize(sb, adminEvent.getAuthDetails().getUserId()); + sb.append(", ipAddress="); + sanitize(sb, adminEvent.getAuthDetails().getIpAddress()); + sb.append(", resourceType="); + sanitize(sb, adminEvent.getResourceTypeAsString()); + sb.append(", resourcePath="); + sanitize(sb, adminEvent.getResourcePath()); + + if (adminEvent.getError() != null) { + sb.append(", error="); + sanitize(sb, adminEvent.getError()); + } + + if (adminEvent.getDetails() != null) { + for (Map.Entry e : adminEvent.getDetails().entrySet()) { + sb.append(", "); + sb.append(StringUtil.sanitizeSpacesAndQuotes(e.getKey(), null)); + sb.append("="); + sanitize(sb, e.getValue()); + } + } + + if(logger.isTraceEnabled()) { + setKeycloakContext(sb); + } + + logger.log(logger.isTraceEnabled() ? Logger.Level.TRACE : level, sb.toString()); + } + } + + @Override + public void close() { + } + + private void setKeycloakContext(StringBuilder sb) { + KeycloakContext context = session.getContext(); + UriInfo uriInfo = context.getUri(); + HttpHeaders headers = context.getRequestHeaders(); + if (uriInfo != null) { + sb.append(", requestUri="); + sanitize(sb, uriInfo.getRequestUri().toString()); + } + + if (headers != null) { + sb.append(", cookies=["); + boolean f = true; + for (Map.Entry e : headers.getCookies().entrySet()) { + if (f) { + f = false; + } else { + sb.append(", "); + } + sb.append(StringUtil.sanitizeSpacesAndQuotes(e.getValue().toString(), null)); + } + sb.append("]"); + } + + } + + } + \ No newline at end of file diff --git a/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProviderFactory.java b/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProviderFactory.java index 0732ff5..eff9de1 100644 --- a/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProviderFactory.java +++ b/src/main/java/org/keycloakify/keycloak/event/logger/AdminEventListenerProviderFactory.java @@ -1,27 +1,95 @@ -package org.keycloakify.keycloak.event.logger; - -import org.keycloak.events.EventListenerProvider; -import org.keycloak.events.EventListenerProviderFactory; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; - -import org.keycloak.Config; - -public class AdminEventListenerProviderFactory implements EventListenerProviderFactory { - - public EventListenerProvider create(KeycloakSession session){ - return new AdminEventListenerProvider(); - } - public void init(Config.Scope config){ - } - - public void postInit(KeycloakSessionFactory factory){ } - - public void close(){ } - - public String getId(){ - return "keycloak-admin-events-logger"; - } +package org.keycloakify.keycloak.event.logger; -} \ No newline at end of file + import java.util.Arrays; + import java.util.Comparator; + import java.util.List; + import org.jboss.logging.Logger; + import org.keycloak.Config; + import org.keycloak.events.EventListenerProviderFactory; + import org.keycloak.models.KeycloakSession; + import org.keycloak.models.KeycloakSessionFactory; + import org.keycloak.provider.ProviderConfigProperty; + import org.keycloak.provider.ProviderConfigurationBuilder; + + public class AdminEventListenerProviderFactory implements EventListenerProviderFactory { + + public static final String ID = "keycloak-admin-events-logger"; + + private static final Logger logger = Logger.getLogger("org.keycloakify.keycloak.event.logger"); + + private Logger.Level successLevel; + private Logger.Level errorLevel; + private boolean sanitize; + private Character quotes; + + @Override + public AdminEventListenerProvider create(KeycloakSession session) { + return new AdminEventListenerProvider(session, logger, successLevel, errorLevel, quotes, sanitize); + } + + @Override + public void init(Config.Scope config) { + successLevel = Logger.Level.valueOf(config.get("success-level", "debug").toUpperCase()); + errorLevel = Logger.Level.valueOf(config.get("error-level", "warn").toUpperCase()); + sanitize = config.getBoolean("sanitize", true); + String quotesString = config.get("quotes", "\""); + if (!quotesString.equals("none") && quotesString.length() > 1) { + logger.warn("Invalid quotes configuration, it should be none or one character to use as quotes. Using default \" quotes"); + quotesString = "\""; + } + quotes = quotesString.equals("none")? null : quotesString.charAt(0); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + + @Override + public List getConfigMetadata() { + String[] logLevels = Arrays.stream(Logger.Level.values()) + .map(Logger.Level::name) + .map(String::toLowerCase) + .sorted(Comparator.naturalOrder()) + .toArray(String[]::new); + return ProviderConfigurationBuilder.create() + .property() + .name("success-level") + .type("string") + .helpText("The log level for success messages.") + .options(logLevels) + .defaultValue("debug") + .add() + .property() + .name("error-level") + .type("string") + .helpText("The log level for error messages.") + .options(logLevels) + .defaultValue("warn") + .add() + .property() + .name("sanitize") + .type("boolean") + .helpText("If true the log messages are sanitized to avoid line breaks. If false messages are not sanitized.") + .defaultValue("true") + .add() + .property() + .name("quotes") + .type("string") + .helpText("The quotes to use for values, it should be one character like \" or '. Use \"none\" if quotes are not needed.") + .defaultValue("\"") + .add() + .build(); + } + } + \ No newline at end of file