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 0be7295fd8..f51d53b058 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 @@ -50,7 +50,8 @@ public class JpaAdminEventQuery implements AdminEventQuery { private final ArrayList predicates; private Integer firstResult; private Integer maxResults; - + private boolean orderByDescTime = true; + public JpaAdminEventQuery(EntityManager em) { this.em = em; @@ -143,13 +144,29 @@ public class JpaAdminEventQuery implements AdminEventQuery { return this; } + @Override + public AdminEventQuery orderByDescTime() { + orderByDescTime = true; + return this; + } + + @Override + public AdminEventQuery orderByAscTime() { + orderByDescTime = false; + return this; + } + @Override public Stream getResultStream() { if (!predicates.isEmpty()) { cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); } - cq.orderBy(cb.desc(root.get("time"))); + if (orderByDescTime) { + cq.orderBy(cb.desc(root.get("time"))); + } else { + cq.orderBy(cb.asc(root.get("time"))); + } TypedQuery query = em.createQuery(cq); 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 aeda806ed6..6e504f8ad8 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 @@ -48,6 +48,7 @@ public class JpaEventQuery implements EventQuery { private final ArrayList predicates; private Integer firstResult; private Integer maxResults; + private boolean orderByDescTime = true; public JpaEventQuery(EntityManager em) { this.em = em; @@ -116,13 +117,29 @@ public class JpaEventQuery implements EventQuery { return this; } + @Override + public EventQuery orderByDescTime() { + orderByDescTime = true; + return this; + } + + @Override + public EventQuery orderByAscTime() { + orderByDescTime = false; + return this; + } + @Override public Stream getResultStream() { if (!predicates.isEmpty()) { cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); } - cq.orderBy(cb.desc(root.get("time"))); + if(orderByDescTime) { + cq.orderBy(cb.desc(root.get("time"))); + } else { + cq.orderBy(cb.asc(root.get("time"))); + } TypedQuery query = em.createQuery(cq); diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java b/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java index d2d33a2a76..a7e4413481 100644 --- a/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java @@ -35,6 +35,7 @@ import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.GE; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.IN; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.LE; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.LIKE; +import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING; import static org.keycloak.models.map.storage.QueryParameters.Order.DESCENDING; import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; @@ -42,6 +43,7 @@ public class MapAdminEventQuery implements AdminEventQuery { private Integer firstResult; private Integer maxResults; + private QueryParameters.Order order = DESCENDING; private DefaultModelCriteria mcb = criteria(); private final Function, Stream> resultProducer; @@ -121,12 +123,24 @@ public class MapAdminEventQuery implements AdminEventQuery { return this; } + @Override + public AdminEventQuery orderByDescTime() { + order = DESCENDING; + return this; + } + + @Override + public AdminEventQuery orderByAscTime() { + order = ASCENDING; + return this; + } + @Override public Stream getResultStream() { return resultProducer.apply(QueryParameters.withCriteria(mcb) .offset(firstResult) .limit(maxResults) - .orderBy(SearchableFields.TIMESTAMP, DESCENDING) + .orderBy(SearchableFields.TIMESTAMP, order) ); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java b/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java index 7afbe2a89e..5840b85a38 100644 --- a/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java @@ -33,6 +33,7 @@ import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.EQ; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.GE; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.IN; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.LE; +import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING; import static org.keycloak.models.map.storage.QueryParameters.Order.DESCENDING; import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; @@ -40,6 +41,7 @@ public class MapAuthEventQuery implements EventQuery { private Integer firstResult; private Integer maxResults; + private QueryParameters.Order order = DESCENDING; private DefaultModelCriteria mcb = criteria(); private final Function, Stream> resultProducer; @@ -101,11 +103,23 @@ public class MapAuthEventQuery implements EventQuery { return this; } + @Override + public EventQuery orderByDescTime() { + order = DESCENDING; + return this; + } + + @Override + public EventQuery orderByAscTime() { + order = ASCENDING; + return this; + } + @Override public Stream getResultStream() { return resultProducer.apply(QueryParameters.withCriteria(mcb) .offset(firstResult) .limit(maxResults) - .orderBy(SearchableFields.TIMESTAMP, DESCENDING)); + .orderBy(SearchableFields.TIMESTAMP, order)); } } 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 62063072ce..3a0c8a7b48 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 @@ -90,6 +90,20 @@ public interface EventQuery { */ EventQuery maxResults(int max); + /** + * Order the result by descending time + * + * @return this for method chaining + */ + EventQuery orderByDescTime(); + + /** + * Order the result by ascending time + * + * @return this for method chaining + */ + EventQuery orderByAscTime(); + /** * @deprecated Use {@link #getResultStream() getResultStream} instead. */ 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 47ce3ed819..8999e943b8 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 @@ -127,6 +127,20 @@ public interface AdminEventQuery { */ AdminEventQuery maxResults(int max); + /** + * Order the result by descending time + * + * @return this for method chaining + */ + AdminEventQuery orderByDescTime(); + + /** + * Order the result by ascending time + * + * @return this for method chaining + */ + AdminEventQuery orderByAscTime(); + /** * Executes the query and returns the results * @deprecated Use {@link #getResultStream() getResultStream} instead. diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java index e01e808a14..8d7a288a11 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java @@ -77,7 +77,7 @@ public class ProtectionService { KeycloakSession keycloakSession = authorization.getKeycloakSession(); UserModel serviceAccount = keycloakSession.users().getServiceAccount(client); AdminEventBuilder adminEvent = new AdminEventBuilder(realm, new AdminAuth(realm, identity.getAccessToken(), serviceAccount, client), keycloakSession, clientConnection); - return adminEvent.realm(realm).authClient(client).authUser(serviceAccount); + return adminEvent; } @Path("/permission") diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java index ba0db2b340..088bdc258d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java @@ -55,7 +55,7 @@ public class AdminEventBuilder { this.listeners = new HashMap<>(); updateStore(session); addListeners(session); - + realm(realm); authRealm(auth.getRealm()); authClient(auth.getClient()); authUser(auth.getUser()); 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 be811dad32..b7f83f49ac 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 @@ -143,7 +143,7 @@ public class RealmAdminResource { this.auth = auth; this.realm = realm; this.tokenManager = tokenManager; - this.adminEvent = adminEvent.realm(realm).resource(ResourceType.REALM); + this.adminEvent = adminEvent.resource(ResourceType.REALM); } /** @@ -701,7 +701,7 @@ public class RealmAdminResource { logger.debug("updating realm events config: " + realm.getName()); new RealmManager(session).updateRealmEventsConfig(rep, realm); - adminEvent.operation(OperationType.UPDATE).resource(ResourceType.REALM).realm(realm) + adminEvent.operation(OperationType.UPDATE).resource(ResourceType.REALM) .resourcePath(session.getContext().getUri()).representation(rep) // refresh the builder to consider old and new config .refreshRealmEventsConfig(session) 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 12d0dde73f..67260c22a3 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 @@ -160,6 +160,31 @@ public class AdminEventTest extends AbstractEventTest { assertThat(realm.getAdminEvents(null, null, null, null, null, null, null, null, 0, 1000).size(), is(greaterThanOrEqualTo(110))); } + @Test + public void orderResultsTest() { + RealmResource realm = adminClient.realms().realm("test"); + AdminEventRepresentation firstEvent = new AdminEventRepresentation(); + firstEvent.setOperationType(OperationType.CREATE.toString()); + firstEvent.setAuthDetails(new AuthDetailsRepresentation()); + firstEvent.setRealmId(realm.toRepresentation().getId()); + firstEvent.setTime(System.currentTimeMillis() - 1000); + + AdminEventRepresentation secondEvent = new AdminEventRepresentation(); + secondEvent.setOperationType(OperationType.DELETE.toString()); + secondEvent.setAuthDetails(new AuthDetailsRepresentation()); + secondEvent.setRealmId(realm.toRepresentation().getId()); + secondEvent.setTime(System.currentTimeMillis()); + + 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); + 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()))); + } + + private void checkUpdateRealmEventsConfigEvent(int size) { List events = events(); assertThat(events.size(), is(equalTo(size))); 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 6a3155c43b..06e7b2952a 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 @@ -152,6 +152,30 @@ public class LoginEventsTest extends AbstractEventTest { assertTrue(realm.getEvents(null, null, null, null, null, null, 0, 1000).size() >= 110); } + @Test + public void orderResultsTest() { + RealmResource realm = adminClient.realms().realm("test"); + EventRepresentation firstEvent = new EventRepresentation(); + firstEvent.setRealmId(realm.toRepresentation().getId()); + firstEvent.setType(EventType.LOGIN.toString()); + firstEvent.setTime(System.currentTimeMillis() - 1000); + + EventRepresentation secondEvent = new EventRepresentation(); + secondEvent.setRealmId(realm.toRepresentation().getId()); + 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); + assertEquals(2, events.size()); + assertEquals(EventType.LOGOUT.toString(), events.get(0).getType()); + assertEquals(EventType.LOGIN.toString(), events.get(1).getType()); + + } + /* Removed this test because it takes too long. The default interval for event cleanup is 15 minutes (900 seconds). I don't have time to figure out diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/AdminEventQueryTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/AdminEventQueryTest.java index b2b216e2ff..41a2747fa5 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/AdminEventQueryTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/AdminEventQueryTest.java @@ -16,32 +16,30 @@ */ package org.keycloak.testsuite.model.events; -import org.keycloak.common.ClientConnection; -import org.keycloak.common.util.Time; -import org.keycloak.events.Event; -import org.keycloak.events.EventBuilder; -import org.keycloak.events.EventStoreProvider; -import org.keycloak.events.EventType; -import org.keycloak.events.admin.AdminEvent; -import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; -import org.keycloak.testsuite.model.KeycloakModelTest; -import org.keycloak.testsuite.model.RequireProvider; -import org.keycloak.models.RealmModel; - -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import org.junit.Test; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -/** - * - * @author hmlnarik - */ +import org.junit.Test; +import org.keycloak.common.ClientConnection; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.OperationType; +import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.delegate.ClientModelLazyDelegate; +import org.keycloak.models.utils.UserModelDelegate; +import org.keycloak.services.resources.admin.AdminAuth; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.testsuite.model.KeycloakModelTest; +import org.keycloak.testsuite.model.RequireProvider; + +import java.util.List; +import java.util.stream.Collectors; + @RequireProvider(EventStoreProvider.class) public class AdminEventQueryTest extends KeycloakModelTest { @@ -56,128 +54,71 @@ public class AdminEventQueryTest extends KeycloakModelTest { @Override public void cleanEnvironment(KeycloakSession s) { + EventStoreProvider eventStore = s.getProvider(EventStoreProvider.class); + eventStore.clearAdmin(s.realms().getRealm(realmId)); s.realms().removeRealm(realmId); } - @Test - public void testClear() { - inRolledBackTransaction(null, (session, t) -> { - EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clear(); - }); - } - - private Event createAuthEventForUser(RealmModel realm, String user) { - return new EventBuilder(realm, null, DummyClientConnection.DUMMY_CONNECTION) - .event(EventType.LOGIN) - .user(user) - .getEvent(); - } - @Test public void testQuery() { withRealm(realmId, (session, realm) -> { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.onEvent(createAuthEventForUser(realm,"u1")); - eventStore.onEvent(createAuthEventForUser(realm,"u2")); - eventStore.onEvent(createAuthEventForUser(realm,"u3")); - eventStore.onEvent(createAuthEventForUser(realm,"u4")); - - return realm.getId(); + eventStore.onEvent(createClientEvent(realm, OperationType.CREATE), false); + eventStore.onEvent(createClientEvent(realm, OperationType.UPDATE), false); + eventStore.onEvent(createClientEvent(realm, OperationType.DELETE), false); + eventStore.onEvent(createClientEvent(realm, OperationType.CREATE), false); + return null; }); withRealm(realmId, (session, realm) -> { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - assertThat(eventStore.createQuery() - .firstResult(2) - .getResultStream() - .collect(Collectors.counting()), - is(2L) - ); - + assertThat(eventStore.createAdminQuery() + .firstResult(2) + .getResultStream() + .collect(Collectors.counting()), + is(2L)); return null; }); } @Test - @RequireProvider(value = EventStoreProvider.class, only = "map") - public void testEventExpiration() { + public void testQueryOrder() { withRealm(realmId, (session, realm) -> { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + AdminEvent firstEvent = createClientEvent(realm, OperationType.CREATE); + firstEvent.setTime(1L); + AdminEvent secondEvent = createClientEvent(realm, OperationType.DELETE); + secondEvent.setTime(2L); + eventStore.onEvent(firstEvent, false); + eventStore.onEvent(secondEvent, false); + List adminEventsAsc = eventStore.createAdminQuery() + .orderByAscTime() + .getResultStream() + .collect(Collectors.toList()); + assertThat(adminEventsAsc.size(), is(2)); + assertThat(adminEventsAsc.get(0).getOperationType(), is(OperationType.CREATE)); + assertThat(adminEventsAsc.get(1).getOperationType(), is(OperationType.DELETE)); - // Set expiration so no event is valid - realm.setEventsExpiration(5); - Event e = createAuthEventForUser(realm, "u1"); - eventStore.onEvent(e); - - // Set expiration to 1000 seconds - realm.setEventsExpiration(1000); - e = createAuthEventForUser(realm, "u2"); - eventStore.onEvent(e); - + List adminEventsDesc = eventStore.createAdminQuery() + .orderByDescTime() + .getResultStream() + .collect(Collectors.toList()); + assertThat(adminEventsDesc.size(), is(2)); + assertThat(adminEventsDesc.get(0).getOperationType(), is(OperationType.DELETE)); + assertThat(adminEventsDesc.get(1).getOperationType(), is(OperationType.CREATE)); return null; }); - - Time.setOffset(10); - - try { - withRealm(realmId, (session, realm) -> { - EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - - Set events = eventStore.createQuery() - .getResultStream().collect(Collectors.toSet()); - - assertThat(events, hasSize(1)); - assertThat(events.iterator().next().getUserId(), equalTo("u2")); - return null; - }); - } finally { - Time.setOffset(0); - } - - } - @Test - @RequireProvider(value = EventStoreProvider.class, only = "map") - public void testEventsClearedOnRealmRemoval() { - // Create another realm - String newRealmId = inComittedTransaction(null, (session, t) -> { - RealmModel realm = session.realms().createRealm("events-realm"); - realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); - - EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - Event e = createAuthEventForUser(realm, "u1"); - eventStore.onEvent(e); - - AdminEvent ae = new AdminEvent(); - ae.setRealmId(realm.getId()); - eventStore.onEvent(ae, false); - - return realm.getId(); - }); - - // Check if events were created - inComittedTransaction(session -> { - EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - assertThat(eventStore.createQuery().getResultStream().count(), is(1L)); - assertThat(eventStore.createAdminQuery().getResultStream().count(), is(1L)); - }); - - // Remove realm - inComittedTransaction((Consumer) session -> session.realms().removeRealm(newRealmId)); - - // Check events were removed - inComittedTransaction(session -> { - EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - assertThat(eventStore.createQuery().getResultStream().count(), is(0L)); - assertThat(eventStore.createAdminQuery().getResultStream().count(), is(0L)); - }); + private AdminEvent createClientEvent(RealmModel realm, OperationType operation) { + return new AdminEventBuilder(realm, new DummyAuth(realm), null, DummyClientConnection.DUMMY_CONNECTION) + .resource(ResourceType.CLIENT).operation(operation).getEvent(); } private static class DummyClientConnection implements ClientConnection { - private static DummyClientConnection DUMMY_CONNECTION = new DummyClientConnection(); + private static final AdminEventQueryTest.DummyClientConnection DUMMY_CONNECTION = + new AdminEventQueryTest.DummyClientConnection(); @Override public String getRemoteAddr() { @@ -205,4 +146,34 @@ public class AdminEventQueryTest extends KeycloakModelTest { } } + private static class DummyAuth extends AdminAuth { + + private final RealmModel realm; + + public DummyAuth(RealmModel realm) { + super(realm, null, null, null); + this.realm = realm; + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientModel getClient() { + return new ClientModelLazyDelegate.WithId("dummy-client", null); + } + + @Override + public UserModel getUser() { + return new UserModelDelegate(null) { + @Override + public String getId() { + return "dummy-user"; + } + }; + } + } + } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/EventQueryTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/EventQueryTest.java new file mode 100644 index 0000000000..c90b402699 --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/events/EventQueryTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2020 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.keycloak.testsuite.model.events; + +import org.keycloak.common.ClientConnection; +import org.keycloak.common.util.Time; +import org.keycloak.events.Event; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.EventType; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.testsuite.model.KeycloakModelTest; +import org.keycloak.testsuite.model.RequireProvider; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +/** + * + * @author hmlnarik + */ +@RequireProvider(EventStoreProvider.class) +public class EventQueryTest extends KeycloakModelTest { + + private String realmId; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("realm"); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + this.realmId = realm.getId(); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + s.realms().removeRealm(realmId); + } + + @Test + public void testClear() { + inRolledBackTransaction(null, (session, t) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + eventStore.clear(); + }); + } + + private Event createAuthEventForUser(RealmModel realm, String user) { + return new EventBuilder(realm, null, DummyClientConnection.DUMMY_CONNECTION) + .event(EventType.LOGIN) + .user(user) + .getEvent(); + } + + @Test + public void testQuery() { + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + + eventStore.onEvent(createAuthEventForUser(realm,"u1")); + eventStore.onEvent(createAuthEventForUser(realm,"u2")); + eventStore.onEvent(createAuthEventForUser(realm,"u3")); + eventStore.onEvent(createAuthEventForUser(realm,"u4")); + + return realm.getId(); + }); + + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + assertThat(eventStore.createQuery() + .firstResult(2) + .getResultStream() + .collect(Collectors.counting()), + is(2L) + ); + + return null; + }); + } + + @Test + public void testQueryOrder() { + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + + Event firstEvent = createAuthEventForUser(realm, "u1"); + firstEvent.setTime(1L); + Event secondEvent = createAuthEventForUser(realm, "u2"); + secondEvent.setTime(2L); + eventStore.onEvent(firstEvent); + eventStore.onEvent(secondEvent); + + return realm.getId(); + }); + + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + List eventsAsc = eventStore.createQuery() + .realm(realmId) + .orderByAscTime() + .getResultStream() + .collect(Collectors.toList()); + assertThat(eventsAsc.size(), is(2)); + assertThat(eventsAsc.get(0).getUserId(), is("u1")); + assertThat(eventsAsc.get(1).getUserId(), is("u2")); + + List eventsDesc = eventStore.createQuery() + .realm(realmId) + .orderByDescTime() + .getResultStream() + .collect(Collectors.toList()); + assertThat(eventsDesc.size(), is(2)); + assertThat(eventsDesc.get(0).getUserId(), is("u2")); + assertThat(eventsDesc.get(1).getUserId(), is("u1")); + + return null; + }); + } + + + @Test + @RequireProvider(value = EventStoreProvider.class, only = "map") + public void testEventExpiration() { + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + + // Set expiration so no event is valid + realm.setEventsExpiration(5); + Event e = createAuthEventForUser(realm, "u1"); + eventStore.onEvent(e); + + // Set expiration to 1000 seconds + realm.setEventsExpiration(1000); + e = createAuthEventForUser(realm, "u2"); + eventStore.onEvent(e); + + return null; + }); + + Time.setOffset(10); + + try { + withRealm(realmId, (session, realm) -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + + Set events = eventStore.createQuery() + .getResultStream().collect(Collectors.toSet()); + + assertThat(events, hasSize(1)); + assertThat(events.iterator().next().getUserId(), equalTo("u2")); + return null; + }); + } finally { + Time.setOffset(0); + } + + + } + + @Test + @RequireProvider(value = EventStoreProvider.class, only = "map") + public void testEventsClearedOnRealmRemoval() { + // Create another realm + String newRealmId = inComittedTransaction(null, (session, t) -> { + RealmModel realm = session.realms().createRealm("events-realm"); + realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + Event e = createAuthEventForUser(realm, "u1"); + eventStore.onEvent(e); + + AdminEvent ae = new AdminEvent(); + ae.setRealmId(realm.getId()); + eventStore.onEvent(ae, false); + + return realm.getId(); + }); + + // Check if events were created + inComittedTransaction(session -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + assertThat(eventStore.createQuery().getResultStream().count(), is(1L)); + assertThat(eventStore.createAdminQuery().getResultStream().count(), is(1L)); + }); + + // Remove realm + inComittedTransaction((Consumer) session -> session.realms().removeRealm(newRealmId)); + + // Check events were removed + inComittedTransaction(session -> { + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + assertThat(eventStore.createQuery().getResultStream().count(), is(0L)); + assertThat(eventStore.createAdminQuery().getResultStream().count(), is(0L)); + }); + } + + private static class DummyClientConnection implements ClientConnection { + + private static DummyClientConnection DUMMY_CONNECTION = new DummyClientConnection(); + + @Override + public String getRemoteAddr() { + return "remoteAddr"; + } + + @Override + public String getRemoteHost() { + return "remoteHost"; + } + + @Override + public int getRemotePort() { + return -1; + } + + @Override + public String getLocalAddr() { + return "localAddr"; + } + + @Override + public int getLocalPort() { + return -2; + } + } + +}