Refactor events to use event store instead of syslog (#36684)

Closes #35091

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2025-01-23 07:40:49 +01:00 committed by GitHub
parent 5387aef0fa
commit ec2c701a80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 333 additions and 130 deletions

View File

@ -8,4 +8,9 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectAdminEvents {
String ref() default "";
String realmRef() default "";
}

View File

@ -8,4 +8,9 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectEvents {
String ref() default "";
String realmRef() default "";
}

View File

@ -0,0 +1,102 @@
package org.keycloak.testframework.events;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.testframework.realm.ManagedRealm;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public abstract class AbstractEvents<E, R> {
protected final ManagedRealm realm;
protected final LinkedList<R> events = new LinkedList<>();
protected final Set<String> processedEvents = new HashSet<>();
protected long testStarted;
protected long timeOffset;
protected long lastFetch;
public AbstractEvents(ManagedRealm realm) {
this.realm = realm;
}
public E poll() {
long currentTimeOffset = getCurrentTimeOffset();
if (timeOffset != currentTimeOffset) {
getLogger().debugv("Timeoffset changed to {0}, resetting events", timeOffset);
events.clear();
timeOffset = currentTimeOffset;
lastFetch = -1;
}
if (events.isEmpty()) {
long from = lastFetch != -1 ? lastFetch : testStarted + currentTimeOffset;
long to = getCurrentTime() + currentTimeOffset;
Logger logger = getLogger();
if (logger.isDebugEnabled()) {
getLogger().debugv("Fetching events from server between {0} and {1}" + (timeOffset != 0 ? "; current timeoffset is {2}" : ""), formatDate(from), formatDate(to), timeOffset);
}
getEvents(from, to)
.stream().filter(e -> !processedEvents.contains(getId(e)))
.forEach(e -> {
processedEvents.add(getId(e));
this.events.add(e);
});
lastFetch = to;
}
R rep = events.poll();
return rep != null ? convert(rep) : null;
}
public void clear() {
events.clear();
clearServerEvents();
}
void testStarted() {
testStarted = getCurrentTime();
timeOffset = getCurrentTimeOffset();
lastFetch = -1;
}
protected abstract List<R> getEvents(long from, long to);
protected String getRealmName(String realmId) {
if (realm.getId().equals(realmId)) {
return realm.getName();
} else {
return null;
}
}
protected abstract String getId(R representation);
protected abstract E convert(R representation);
protected abstract void clearServerEvents();
protected abstract Logger getLogger();
protected long getCurrentTime() {
return System.currentTimeMillis();
}
protected long getCurrentTimeOffset() {
return TimeUnit.MILLISECONDS.convert(Time.getOffset(), TimeUnit.SECONDS);
}
protected String formatDate(long timestamp) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS").format(timestamp);
}
}

View File

@ -0,0 +1,45 @@
package org.keycloak.testframework.events;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.injection.SupplierHelpers;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfigInterceptor;
import java.lang.annotation.Annotation;
@SuppressWarnings("rawtypes")
public abstract class AbstractEventsSupplier<E extends AbstractEvents, A extends Annotation> implements Supplier<E, A>, RealmConfigInterceptor<E, A> {
@Override
public E getValue(InstanceContext<E, A> instanceContext) {
String realmRef = SupplierHelpers.getAnnotationField(instanceContext.getAnnotation(), "realmRef");
ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class, realmRef);
return createValue(realm);
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.GLOBAL;
}
@Override
public boolean compatible(InstanceContext<E, A> a, RequestedInstance<E, A> b) {
return true;
}
@Override
public void onBeforeEach(InstanceContext<E, A> instanceContext) {
instanceContext.getValue().testStarted();
}
@Override
public void close(InstanceContext<E, A> instanceContext) {
instanceContext.getValue().clear();
}
protected abstract E createValue(ManagedRealm realm);
}

View File

@ -1,32 +1,68 @@
package org.keycloak.testframework.events;
import org.jboss.logging.Logger;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.AuthDetails;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthDetailsRepresentation;
import org.keycloak.testframework.realm.ManagedRealm;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.List;
public class AdminEvents implements SysLogListener{
public class AdminEvents extends AbstractEvents<AdminEvent, AdminEventRepresentation> {
private final BlockingQueue<AdminEvent> adminEvents = new LinkedBlockingQueue<>();
private static final Logger LOGGER = Logger.getLogger(AdminEvents.class);
public AdminEvent poll() {
try {
return adminEvents.poll(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return null;
}
}
public void clear() {
adminEvents.clear();
public AdminEvents(ManagedRealm realm) {
super(realm);
}
@Override
public void onLog(SysLog sysLog) {
AdminEvent adminEvent = AdminEventsParser.parse(sysLog);
if (adminEvent != null) {
adminEvents.add(adminEvent);
}
protected List<AdminEventRepresentation> getEvents(long from, long to) {
return realm.admin().getAdminEvents(null, null, null, null, null, null, null, from, to, null, null, "asc");
}
@Override
protected String getId(AdminEventRepresentation rep) {
return rep.getId();
}
@Override
protected AdminEvent convert(AdminEventRepresentation rep) {
AdminEvent e = new AdminEvent();
e.setId(rep.getId());
e.setTime(rep.getTime());
e.setRealmId(rep.getRealmId());
e.setRealmName(getRealmName(rep.getRealmId()));
e.setAuthDetails(convert(rep.getAuthDetails()));
e.setResourceType(ResourceType.valueOf(rep.getResourceType()));
e.setOperationType(OperationType.valueOf(rep.getOperationType()));
e.setResourcePath(rep.getResourcePath());
e.setRepresentation(rep.getRepresentation());
e.setError(rep.getError());
e.setDetails(rep.getDetails());
return e;
}
private AuthDetails convert(AuthDetailsRepresentation rep) {
AuthDetails d = new AuthDetails();
d.setClientId(rep.getClientId());
d.setIpAddress(rep.getIpAddress());
d.setRealmId(rep.getRealmId());
d.setRealmName(getRealmName(rep.getRealmId()));
d.setUserId(rep.getUserId());
return d;
}
@Override
protected void clearServerEvents() {
realm.admin().clearAdminEvents();
}
@Override
protected Logger getLogger() {
return LOGGER;
}
}

View File

@ -2,12 +2,10 @@ package org.keycloak.testframework.events;
import org.keycloak.testframework.annotations.InjectAdminEvents;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.injection.SupplierOrder;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfigBuilder;
public class AdminEventsSupplier implements Supplier<AdminEvents, InjectAdminEvents> {
public class AdminEventsSupplier extends AbstractEventsSupplier<AdminEvents, InjectAdminEvents> {
@Override
public Class<InjectAdminEvents> getAnnotationClass() {
@ -20,35 +18,13 @@ public class AdminEventsSupplier implements Supplier<AdminEvents, InjectAdminEve
}
@Override
public AdminEvents getValue(InstanceContext<AdminEvents, InjectAdminEvents> instanceContext) {
AdminEvents adminEvents = new AdminEvents();
SysLogServer sysLogServer = instanceContext.getDependency(SysLogServer.class);
instanceContext.addNote("server", sysLogServer);
return adminEvents;
public AdminEvents createValue(ManagedRealm realm) {
return new AdminEvents(realm);
}
@Override
public void onBeforeEach(InstanceContext<AdminEvents, InjectAdminEvents> instanceContext) {
instanceContext.getNote("server", SysLogServer.class).addListener(instanceContext.getValue());
public RealmConfigBuilder intercept(RealmConfigBuilder realm, InstanceContext<AdminEvents, InjectAdminEvents> instanceContext) {
return realm.adminEventsEnabled(true);
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.METHOD;
}
@Override
public void close(InstanceContext<AdminEvents, InjectAdminEvents> instanceContext) {
instanceContext.getNote("server", SysLogServer.class).removeListener(instanceContext.getValue());
}
@Override
public boolean compatible(InstanceContext<AdminEvents, InjectAdminEvents> a, RequestedInstance<AdminEvents, InjectAdminEvents> b) {
return true;
}
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
}
}

View File

@ -1,32 +1,56 @@
package org.keycloak.testframework.events;
import org.jboss.logging.Logger;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.testframework.realm.ManagedRealm;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.List;
public class Events implements SysLogListener {
public class Events extends AbstractEvents<Event, EventRepresentation> {
private final BlockingQueue<Event> events = new LinkedBlockingQueue<>();
private static final Logger LOGGER = Logger.getLogger(Events.class);
public Event poll() {
try {
return events.poll(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return null;
}
}
public void clear() {
events.clear();
public Events(ManagedRealm realm) {
super(realm);
}
@Override
public void onLog(SysLog sysLog) {
Event event = EventParser.parse(sysLog);
if (event != null) {
events.add(event);
}
protected List<EventRepresentation> getEvents(long from, long to) {
return realm.admin().getEvents(null, null, null, from, to, null, null, null, "asc");
}
@Override
protected String getId(EventRepresentation rep) {
return rep.getId();
}
@Override
protected Event convert(EventRepresentation rep) {
Event e = new Event();
e.setId(rep.getId());
e.setTime(rep.getTime());
e.setType(EventType.valueOf(rep.getType()));
e.setRealmId(rep.getRealmId());
e.setRealmName(getRealmName(rep.getRealmId()));
e.setClientId(rep.getClientId());
e.setUserId(rep.getUserId());
e.setSessionId(rep.getSessionId());
e.setIpAddress(rep.getIpAddress());
e.setError(rep.getError());
e.setDetails(rep.getDetails());
return e;
}
@Override
protected void clearServerEvents() {
realm.admin().clearEvents();
}
@Override
protected Logger getLogger() {
return LOGGER;
}
}

View File

@ -2,12 +2,10 @@ package org.keycloak.testframework.events;
import org.keycloak.testframework.annotations.InjectEvents;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.injection.SupplierOrder;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfigBuilder;
public class EventsSupplier implements Supplier<Events, InjectEvents> {
public class EventsSupplier extends AbstractEventsSupplier<Events, InjectEvents> {
@Override
public Class<InjectEvents> getAnnotationClass() {
@ -20,35 +18,13 @@ public class EventsSupplier implements Supplier<Events, InjectEvents> {
}
@Override
public Events getValue(InstanceContext<Events, InjectEvents> instanceContext) {
Events events = new Events();
SysLogServer sysLogServer = instanceContext.getDependency(SysLogServer.class);
instanceContext.addNote("server", sysLogServer);
return events;
protected Events createValue(ManagedRealm realm) {
return new Events(realm);
}
@Override
public void onBeforeEach(InstanceContext<Events, InjectEvents> instanceContext) {
instanceContext.getNote("server", SysLogServer.class).addListener(instanceContext.getValue());
public RealmConfigBuilder intercept(RealmConfigBuilder realm, InstanceContext<Events, InjectEvents> instanceContext) {
return realm.eventsEnabled(true);
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.METHOD;
}
@Override
public void close(InstanceContext<Events, InjectEvents> instanceContext) {
instanceContext.getNote("server", SysLogServer.class).removeListener(instanceContext.getValue());
}
@Override
public boolean compatible(InstanceContext<Events, InjectEvents> a, RequestedInstance<Events, InjectEvents> b) {
return true;
}
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
}
}

View File

@ -72,6 +72,16 @@ public class RealmConfigBuilder {
return this;
}
public RealmConfigBuilder eventsEnabled(boolean enabled) {
rep.setEventsEnabled(enabled);
return this;
}
public RealmConfigBuilder adminEventsEnabled(boolean enabled) {
rep.setAdminEventsEnabled(enabled);
return this;
}
public RealmConfigBuilder eventsListeners(String... eventListeners) {
if (rep.getEventsListeners() == null) {
rep.setEventsListeners(new LinkedList<>());

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testframework.annotations.InjectAdminClient;
@ -13,21 +14,28 @@ import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.AdminEvents;
import org.keycloak.testframework.realm.ManagedRealm;
import java.util.List;
@KeycloakIntegrationTest
public class AdminEventsTest {
@InjectAdminEvents
private AdminEvents adminEvents;
@InjectAdminClient
private Keycloak adminClient;
@InjectRealm
private ManagedRealm realm;
@InjectAdminEvents
private AdminEvents adminEvents;
@InjectRealm(ref = "master", attachTo = "master")
private ManagedRealm master;
@InjectAdminEvents(ref = "master", realmRef = "master")
private AdminEvents masterAdminEvents;
@InjectAdminClient
private Keycloak adminClient;
@Test
public void testAdminEventOnUserCreateAndDelete() {
String userName = "create_user";
UserRepresentation userRep = new UserRepresentation();
@ -47,21 +55,23 @@ public class AdminEventsTest {
@Test
public void testAdminEventOnRealmCreateAndUpdate() {
master.updateWithCleanup(r -> r.adminEventsEnabled(true));
String realmName = "create_realm";
RealmRepresentation realmRep = new RealmRepresentation();
realmRep.setRealm(realmName);
realmRep.setEnabled(true);
realmRep.setAdminEventsEnabled(true);
adminClient.realms().create(realmRep);
Assertions.assertEquals(OperationType.CREATE, adminEvents.poll().getOperationType());
Assertions.assertEquals(OperationType.CREATE, masterAdminEvents.poll().getOperationType());
}
RealmRepresentation realmRep2 = adminClient.realms().realm(realmName).toRepresentation();
realmRep2.setEnabled(false);
adminClient.realms().realm(realmName).update(realmRep2);
@Test
public void testAdminEventOnRealmUpdate() {
realm.updateWithCleanup(r -> r.editUsernameAllowed(true));
Assertions.assertEquals(OperationType.UPDATE, adminEvents.poll().getOperationType());
}

View File

@ -3,43 +3,57 @@ package org.keycloak.test.examples;
import com.nimbusds.oauth2.sdk.GeneralException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.testframework.annotations.InjectEvents;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.Events;
import org.keycloak.testframework.oauth.nimbus.OAuthClient;
import org.keycloak.testframework.oauth.nimbus.annotations.InjectOAuthClient;
import org.keycloak.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
import org.keycloak.testframework.ui.page.LoginPage;
import org.openqa.selenium.WebDriver;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
import java.io.IOException;
import java.net.URL;
@KeycloakIntegrationTest
public class EventsTest {
@InjectRealm
private ManagedRealm realm;
@InjectEvents
private Events events;
@InjectOAuthClient
private OAuthClient oAuthClient;
@InjectWebDriver
private WebDriver webDriver;
@InjectPage
private LoginPage loginPage;
@InjectTimeOffSet
TimeOffSet timeOffSet;
@Test
public void testFailedLogin() throws GeneralException, IOException {
URL authorizationRequestURL = oAuthClient.authorizationRequest();
webDriver.navigate().to(authorizationRequestURL);
loginPage.fillLogin("invalid", "invalid");
loginPage.submit();
public void testFailedLogin() {
oAuthClient.resourceOwnerCredentialGrant("invalid", "invalid");
Assertions.assertEquals(EventType.LOGIN_ERROR, events.poll().getType());
Event event = events.poll();
Assertions.assertEquals(EventType.LOGIN_ERROR, event.getType());
Assertions.assertEquals("invalid", event.getDetails().get("username"));
oAuthClient.resourceOwnerCredentialGrant("invalid2", "invalid");
event = events.poll();
Assertions.assertEquals(EventType.LOGIN_ERROR, event.getType());
Assertions.assertEquals("invalid2", event.getDetails().get("username"));
}
@Test
public void testTimeOffset() throws GeneralException, IOException {
timeOffSet.set(60);
oAuthClient.clientCredentialGrant();
Assertions.assertEquals(EventType.CLIENT_LOGIN, events.poll().getType());
}
@Test