diff --git a/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java index d6687e8f321..f015762288b 100644 --- a/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/AdminEventRepresentation.java @@ -24,6 +24,7 @@ import java.util.Map; */ public class AdminEventRepresentation { + private String id; private long time; private String realmId; private AuthDetailsRepresentation authDetails; @@ -34,6 +35,14 @@ public class AdminEventRepresentation { private String error; private Map details; + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public long getTime() { return time; } diff --git a/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java index 0621b9b040f..2ecf06240ba 100644 --- a/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/EventRepresentation.java @@ -24,6 +24,7 @@ import java.util.Map; */ public class EventRepresentation { + private String id; private long time; private String type; private String realmId; @@ -34,6 +35,14 @@ public class EventRepresentation { private String error; private Map details; + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public long getTime() { return time; } @@ -128,6 +137,7 @@ public class EventRepresentation { @Override public int hashCode() { int result = (int) (time ^ (time >>> 32)); + result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + (realmId != null ? realmId.hashCode() : 0); result = 31 * result + (clientId != null ? clientId.hashCode() : 0); diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index 805b42a5201..d69ecc3a1a5 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -13,6 +13,9 @@ include::topics/templates/document-attributes.adoc[] :release_header_latest_link: {releasenotes_link_latest} include::topics/templates/release-header.adoc[] +== {project_name_full} 26.2.0 +include::topics/26_2_0.adoc[leveloffset=2] + == {project_name_full} 26.1.0 include::topics/26_1_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/topics/26_2_0.adoc b/docs/documentation/release_notes/topics/26_2_0.adoc new file mode 100644 index 00000000000..890756b9c3f --- /dev/null +++ b/docs/documentation/release_notes/topics/26_2_0.adoc @@ -0,0 +1,10 @@ += Additional query parameters in Admin Events API + +The Admin Events API now supports filtering for events based on Epoc timestamps in addition to the previous +`yyyy-MM-dd` format. This provides more fine-grained control of the window of events to retrieve. + +A `direction` query parameter was also added, allowing controlling the order of returned items as `asc` or +`desc`. In the past the events where always returned in `desc` order (most recent events first). + +Finally, the returned event representations now also include the `id`, which provides a unique identifier for +an event. diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index ccb9d5798bb..a030535327f 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -126,6 +126,24 @@ public interface RealmResource { @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + @Path("events") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getEvents(@QueryParam("type") List types, @QueryParam("client") String client, + @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo, + @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("direction") String direction); + + @Path("events") + @GET + @Produces(MediaType.APPLICATION_JSON) + List getEvents(@QueryParam("type") List types, @QueryParam("client") String client, + @QueryParam("user") String user, @QueryParam("dateFrom") long dateFrom, @QueryParam("dateTo") long dateTo, + @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("direction") String direction); + @DELETE @Path("admin-events") void clearAdminEvents(); @@ -153,6 +171,24 @@ public interface RealmResource { @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + @GET + @Path("admin-events") + @Produces(MediaType.APPLICATION_JSON) + List getAdminEvents(@QueryParam("operationTypes") List operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient, + @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, + @QueryParam("resourcePath") String resourcePath, @QueryParam("resourceTypes") List resourceTypes, @QueryParam("dateFrom") String dateFrom, + @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, @QueryParam("direction") String direction); + + @GET + @Path("admin-events") + @Produces(MediaType.APPLICATION_JSON) + List getAdminEvents(@QueryParam("operationTypes") List operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient, + @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, + @QueryParam("resourcePath") String resourcePath, @QueryParam("resourceTypes") List resourceTypes, @QueryParam("dateFrom") long dateFrom, + @QueryParam("dateTo") long dateTo, @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, @QueryParam("direction") String direction); + @GET @Path("events/config") @Produces(MediaType.APPLICATION_JSON) diff --git a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java index b5fed1fad79..8e42293a436 100755 --- a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java +++ b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaAdminEventQuery.java @@ -121,14 +121,26 @@ public class JpaAdminEventQuery implements AdminEventQuery { } @Override + @Deprecated public AdminEventQuery fromTime(Date fromTime) { - predicates.add(cb.greaterThanOrEqualTo(root.get("time"), fromTime.getTime())); + return fromTime(fromTime.getTime()); + } + + @Override + public AdminEventQuery fromTime(long fromTime) { + predicates.add(cb.greaterThanOrEqualTo(root.get("time"), fromTime)); return this; } @Override + @Deprecated public AdminEventQuery toTime(Date toTime) { - predicates.add(cb.lessThanOrEqualTo(root.get("time"), toTime.getTime())); + return toTime(toTime.getTime()); + } + + @Override + public AdminEventQuery toTime(long toTime) { + predicates.add(cb.lessThanOrEqualTo(root.get("time"), toTime)); return this; } diff --git a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java index 74d4b62d382..768444450eb 100644 --- a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java +++ b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventQuery.java @@ -90,8 +90,14 @@ public class JpaEventQuery implements EventQuery { } @Override + @Deprecated public EventQuery fromDate(Date fromDate) { - predicates.add(cb.greaterThanOrEqualTo(root.get("time"), fromDate.getTime())); + return fromDate(fromDate.getTime()); + } + + @Override + public EventQuery fromDate(long fromDate) { + predicates.add(cb.greaterThanOrEqualTo(root.get("time"), fromDate)); return this; } @@ -103,7 +109,12 @@ public class JpaEventQuery implements EventQuery { calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); calendar.set(Calendar.MILLISECOND, 999); - predicates.add(cb.lessThanOrEqualTo(root.get("time"), calendar.getTimeInMillis())); + return toDate(calendar.getTimeInMillis()); + } + + @Override + public EventQuery toDate(long toDate) { + predicates.add(cb.lessThanOrEqualTo(root.get("time"), toDate)); return this; } diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventQuery.java b/server-spi-private/src/main/java/org/keycloak/events/EventQuery.java index db8aa707abf..3c0cce8eba5 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/EventQuery.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventQuery.java @@ -56,19 +56,35 @@ public interface EventQuery { EventQuery user(String userId); /** - * Search events that are newer than {@code fromDate} + * Search events that are on or after {@code fromDate} * @param fromDate date * @return this object for method chaining */ + @Deprecated EventQuery fromDate(Date fromDate); /** - * Search events that are older than {@code toDate} + * Search events that are on or after {@code fromDate} + * @param fromDate from timestamp + * @return this object for method chaining + */ + EventQuery fromDate(long fromDate); + + /** + * Search events that are on or before {@code toDate} * @param toDate date * @return this object for method chaining */ + @Deprecated EventQuery toDate(Date toDate); + /** + * Search events that are on or before {@code toDate} + * @param toDate to timestamp + * @return this object for method chaining + */ + EventQuery toDate(long toDate); + /** * Search events from ipAddress * @param ipAddress ip diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java index a750aa153ee..6d5d9175676 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java +++ b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEventQuery.java @@ -18,8 +18,6 @@ package org.keycloak.events.admin; import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -96,21 +94,39 @@ public interface AdminEventQuery { AdminEventQuery resourcePath(String resourcePath); /** - * Search by events after the specified time + * Search by events on or after the specified time * * @param fromTime from date * @return this for method chaining */ + @Deprecated AdminEventQuery fromTime(Date fromTime); /** - * Search by events before the specified time + * Search by events on or after the specified timestamp + * + * @param fromTime from timestamp + * @return this for method chaining + */ + AdminEventQuery fromTime(long fromTime); + + /** + * Search by events on or before the specified time * * @param toTime to date * @return this for method chaining */ + @Deprecated AdminEventQuery toTime(Date toTime); + /** + * Search by events on or before the specified timestamp + * + * @param toTime to timestamp + * @return this for method chaining + */ + AdminEventQuery toTime(long toTime); + /** * Used for pagination * diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 88f1971eace..3e998570ce1 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -288,6 +288,7 @@ public class ModelToRepresentation { public static EventRepresentation toRepresentation(Event event) { EventRepresentation rep = new EventRepresentation(); + rep.setId(event.getId()); rep.setTime(event.getTime()); rep.setType(event.getType().toString()); rep.setRealmId(event.getRealmId()); @@ -302,6 +303,7 @@ public class ModelToRepresentation { public static AdminEventRepresentation toRepresentation(AdminEvent adminEvent) { AdminEventRepresentation rep = new AdminEventRepresentation(); + rep.setId(adminEvent.getId()); rep.setTime(adminEvent.getTime()); rep.setRealmId(adminEvent.getRealmId()); if (adminEvent.getAuthDetails() != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 7b22c63ad5d..9da60309298 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -16,24 +16,11 @@ */ package org.keycloak.services.resources.admin; -import static org.keycloak.util.JsonSerialization.readValue; - -import java.io.InputStream; -import java.security.cert.X509Certificate; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - +import com.fasterxml.jackson.core.type.TypeReference; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; @@ -48,9 +35,6 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.StreamingOutput; - -import com.fasterxml.jackson.core.type.TypeReference; - import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; @@ -114,6 +98,7 @@ import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.services.util.DateUtil; import org.keycloak.storage.DatastoreProvider; import org.keycloak.storage.ExportImportManager; import org.keycloak.storage.StoreSyncEvent; @@ -121,6 +106,17 @@ import org.keycloak.utils.GroupUtils; import org.keycloak.utils.ProfileHelper; import org.keycloak.utils.ReservedCharValidator; +import java.io.InputStream; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.util.JsonSerialization.readValue; + /** * Base resource class for the admin REST api of one realm * @@ -131,6 +127,7 @@ import org.keycloak.utils.ReservedCharValidator; @Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RealmAdminResource { protected static final Logger logger = Logger.getLogger(RealmAdminResource.class); + protected final AdminPermissionEvaluator auth; protected final RealmModel realm; private final AdminEventBuilder adminEvent; @@ -786,11 +783,12 @@ public class RealmAdminResource { public Stream getEvents(@Parameter(description = "The types of events to return") @QueryParam("type") List types, @Parameter(description = "App or oauth client name") @QueryParam("client") String client, @Parameter(description = "User id") @QueryParam("user") String user, - @Parameter(description = "From date") @QueryParam("dateFrom") String dateFrom, - @Parameter(description = "To date") @QueryParam("dateTo") String dateTo, + @Parameter(description = "From (inclusive) date (yyyy-MM-dd) or time in Epoch timestamp") @QueryParam("dateFrom") String dateFrom, + @Parameter(description = "To (inclusive) date (yyyy-MM-dd) or time in Epoch timestamp") @QueryParam("dateTo") String dateTo, @Parameter(description = "IP Address") @QueryParam("ipAddress") String ipAddress, @Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, - @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults) { + @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, + @Parameter(description = "The direction to sort events by (asc or desc)") @QueryParam("direction") String direction) { auth.realm().requireViewEvents(); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); @@ -813,30 +811,35 @@ public class RealmAdminResource { } if(dateFrom != null) { - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date from = null; try { - from = df.parse(dateFrom); - } catch (ParseException e) { - throw new BadRequestException("Invalid value for 'Date(From)', expected format is yyyy-MM-dd"); + query.fromDate(DateUtil.toStartOfDay(dateFrom)); + } catch (Throwable t) { + throw new BadRequestException("Invalid value for 'dateFrom', expected format is yyyy-MM-dd or an Epoch timestamp"); } - query.fromDate(from); } if(dateTo != null) { - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date to = null; try { - to = df.parse(dateTo); - } catch (ParseException e) { - throw new BadRequestException("Invalid value for 'Date(To)', expected format is yyyy-MM-dd"); + query.toDate(DateUtil.toEndOfDay(dateTo)); + } catch (Throwable t) { + throw new BadRequestException("Invalid value for 'dateTo', expected format is yyyy-MM-dd or an Epoch timestamp"); } - query.toDate(to); } if (ipAddress != null) { query.ipAddress(ipAddress); } + + if (direction != null) { + if ("asc".equals(direction)) { + query.orderByAscTime(); + } else if ("desc".equals(direction)) { + query.orderByDescTime(); + } else { + throw new BadRequestException("Invalid value for sortDirection, expected value is asc or desc"); + } + } + if (firstResult != null) { query.firstResult(firstResult); } @@ -874,10 +877,13 @@ public class RealmAdminResource { @Operation( summary = "Get admin events Returns all admin events, or filters events based on URL query parameters listed here") public Stream getEvents(@QueryParam("operationTypes") List operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient, @Parameter(description = "user id") @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, - @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom, - @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult, + @QueryParam("resourcePath") String resourcePath, + @Parameter(description = "From (inclusive) date (yyyy-MM-dd) or time in Epoch timestamp") @QueryParam("dateFrom") String dateFrom, + @Parameter(description = "To (inclusive) date (yyyy-MM-dd) or time in Epoch timestamp") @QueryParam("dateTo") String dateTo, + @QueryParam("first") Integer firstResult, @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, - @QueryParam("resourceTypes") List resourceTypes) { + @QueryParam("resourceTypes") List resourceTypes, + @Parameter(description = "The direction to sort events by (asc or desc)") @QueryParam("direction") String direction) { auth.realm().requireViewEvents(); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); @@ -919,28 +925,30 @@ public class RealmAdminResource { query.resourceType(t); } - - if(dateFrom != null) { - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date from = null; try { - from = df.parse(dateFrom); - } catch (ParseException e) { - throw new BadRequestException("Invalid value for 'Date(From)', expected format is yyyy-MM-dd"); + query.fromTime(DateUtil.toStartOfDay(dateFrom)); + } catch (Throwable t) { + throw new BadRequestException("Invalid value for 'dateFrom', expected format is yyyy-MM-dd or an Epoch timestamp"); } - query.fromTime(from); } if(dateTo != null) { - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - Date to = null; try { - to = df.parse(dateTo); - } catch (ParseException e) { - throw new BadRequestException("Invalid value for 'Date(To)', expected format is yyyy-MM-dd"); + query.toTime(DateUtil.toEndOfDay(dateTo)); + } catch (Throwable t) { + throw new BadRequestException("Invalid value for 'dateTo', expected format is yyyy-MM-dd or an Epoch timestamp"); + } + } + + if (direction != null) { + if ("asc".equals(direction)) { + query.orderByAscTime(); + } else if ("desc".equals(direction)) { + query.orderByDescTime(); + } else { + throw new BadRequestException("Invalid value for sortDirection, expected value is asc or desc"); } - query.toTime(to); } if (firstResult != null) { diff --git a/services/src/main/java/org/keycloak/services/util/DateUtil.java b/services/src/main/java/org/keycloak/services/util/DateUtil.java new file mode 100644 index 00000000000..0d39f50f09d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/util/DateUtil.java @@ -0,0 +1,39 @@ +package org.keycloak.services.util; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; + +public class DateUtil { + + /** + * Parses a string timestamp or date to an Epoc timestamp; if the date is a ISO-8601 extended local date format, + * the time at the beginning of the day is returned. + * + * @param date the date in ISO-8601 extended local date format + * @return Epoch time for the start of the day + */ + public static long toStartOfDay(String date) { + if (date.indexOf('-') != -1) { + return LocalDate.parse(date).atStartOfDay().toEpochSecond(ZoneOffset.UTC); + } else { + return Long.parseLong(date); + } + } + + /** + * Parses a string timestamp or date to an Epoc timestamp; if the date is a ISO-8601 extended local date format, + * the time at the end of the day is returned. + * + * @param date the date in ISO-8601 extended local date format + * @return Epoch time for the start of the day + */ + public static long toEndOfDay(String date) { + if (date.indexOf('-') != -1) { + return LocalDate.parse(date).atTime(LocalTime.MAX).toEpochSecond(ZoneOffset.UTC); + } else { + return Long.parseLong(date); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java index d6be873c6b3..26caed19d20 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/AdminEventTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.admin.event; import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; @@ -32,6 +33,7 @@ import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.util.JsonSerialization; @@ -135,6 +137,7 @@ public class AdminEventTest extends AbstractEventTest { assertThat(events().size(), is(equalTo(4))); AdminEventRepresentation event = events.get(0); + assertThat(event.getId(), AssertEvents.isUUID()); assertThat(event.getRealmId(), is(equalTo(realmName()))); assertThat(event.getOperationType(), is(equalTo("CREATE"))); assertThat(event.getResourceType(), is(equalTo(ResourceType.ORGANIZATION_MEMBERSHIP.name()))); @@ -233,12 +236,39 @@ public class AdminEventTest extends AbstractEventTest { testingClient.testing("test").onAdminEvent(firstEvent, false); testingClient.testing("test").onAdminEvent(secondEvent, false); - List adminEvents = realm.getAdminEvents(null, null, null, null, null, null, null, null, null, null); + List adminEvents = realm.getAdminEvents(null, null, null, null, null, null, null, null, null, null, null, "desc"); assertThat(adminEvents.size(), is(equalTo(2))); assertThat(adminEvents.get(0).getOperationType(), is(equalTo(OperationType.DELETE.toString()))); assertThat(adminEvents.get(1).getOperationType(), is(equalTo(OperationType.CREATE.toString()))); + + adminEvents = realm.getAdminEvents(null, null, null, null, null, null, null, null, null, null, null, "asc"); + assertThat(adminEvents.size(), is(equalTo(2))); + assertThat(adminEvents.get(1).getOperationType(), is(equalTo(OperationType.DELETE.toString()))); + assertThat(adminEvents.get(0).getOperationType(), is(equalTo(OperationType.CREATE.toString()))); } + @Test + public void filterByEpochTimeStamp() { + RealmResource realm = adminClient.realms().realm("test"); + AdminEventRepresentation event = new AdminEventRepresentation(); + event.setOperationType(OperationType.CREATE.toString()); + event.setAuthDetails(new AuthDetailsRepresentation()); + event.setRealmId(realm.toRepresentation().getId()); + + long currentTime = System.currentTimeMillis(); + + event.setTime(currentTime - 1000); + testingClient.testing("test").onAdminEvent(event, false); + event.setTime(currentTime); + testingClient.testing("test").onAdminEvent(event, false); + event.setTime(currentTime + 1000); + testingClient.testing("test").onAdminEvent(event, false); + + List events = realm.getAdminEvents(null, null, null, null, null, null, null, currentTime, currentTime, null, null, null); + Assert.assertEquals(1, events.size()); + events = realm.getAdminEvents(null, null, null, null, null, null, null, currentTime - 1000, currentTime + 1000, null, null, null); + Assert.assertEquals(3, events.size()); + } private void checkUpdateRealmEventsConfigEvent(int size) { List events = events(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/LoginEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/LoginEventsTest.java index 0b19b53427b..c17dc85aa09 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/LoginEventsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/event/LoginEventsTest.java @@ -17,11 +17,14 @@ package org.keycloak.testsuite.admin.event; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.events.EventType; import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.testsuite.AssertEvents; import java.util.Arrays; import java.util.List; @@ -71,6 +74,7 @@ public class LoginEventsTest extends AbstractEventTest { List events = events(); assertEquals(1, events.size()); EventRepresentation event = events.get(0); + MatcherAssert.assertThat(event.getId(), AssertEvents.isUUID()); assertTrue(event.getTime() > 0); assertNotNull(event.getIpAddress()); assertEquals("LOGIN_ERROR", event.getType()); @@ -155,15 +159,41 @@ public class LoginEventsTest extends AbstractEventTest { secondEvent.setType(EventType.LOGOUT.toString()); secondEvent.setTime(System.currentTimeMillis()); - testingClient.testing("test").onEvent(firstEvent); testingClient.testing("test").onEvent(secondEvent); - List events = realm.getEvents(null, null, null, null, null, null, null, null); + List events = realm.getEvents(null, null, null, null, null, null, null, null, "desc"); assertEquals(2, events.size()); assertEquals(EventType.LOGOUT.toString(), events.get(0).getType()); assertEquals(EventType.LOGIN.toString(), events.get(1).getType()); + events = realm.getEvents(null, null, null, null, null, null, null, null, "asc"); + assertEquals(2, events.size()); + assertEquals(EventType.LOGOUT.toString(), events.get(1).getType()); + assertEquals(EventType.LOGIN.toString(), events.get(0).getType()); + } + + + @Test + public void filterByEpochTimeStamp() { + RealmResource realm = adminClient.realms().realm("test"); + EventRepresentation event = new EventRepresentation(); + event.setType(EventType.LOGIN.toString()); + event.setRealmId(realm.toRepresentation().getId()); + + long currentTime = System.currentTimeMillis(); + + event.setTime(currentTime - 1000); + testingClient.testing("test").onEvent(event); + event.setTime(currentTime); + testingClient.testing("test").onEvent(event); + event.setTime(currentTime + 1000); + testingClient.testing("test").onEvent(event); + + List events = realm.getEvents(null, null, null, currentTime, currentTime, null, null, null, null); + Assert.assertEquals(1, events.size()); + events = realm.getEvents(null, null, null, currentTime - 1000, currentTime + 1000, null, null, null, null); + Assert.assertEquals(3, events.size()); } @Test