Issue #8749: Add an option to control the order of the event query and admin event query

This commit is contained in:
Christoph Leistert 2021-11-15 08:25:26 +01:00 committed by Hynek Mlnařík
parent 1d2d3e5ca5
commit 7e5b45f999
13 changed files with 486 additions and 127 deletions

View file

@ -50,7 +50,8 @@ public class JpaAdminEventQuery implements AdminEventQuery {
private final ArrayList<Predicate> predicates; private final ArrayList<Predicate> predicates;
private Integer firstResult; private Integer firstResult;
private Integer maxResults; private Integer maxResults;
private boolean orderByDescTime = true;
public JpaAdminEventQuery(EntityManager em) { public JpaAdminEventQuery(EntityManager em) {
this.em = em; this.em = em;
@ -143,13 +144,29 @@ public class JpaAdminEventQuery implements AdminEventQuery {
return this; return this;
} }
@Override
public AdminEventQuery orderByDescTime() {
orderByDescTime = true;
return this;
}
@Override
public AdminEventQuery orderByAscTime() {
orderByDescTime = false;
return this;
}
@Override @Override
public Stream<AdminEvent> getResultStream() { public Stream<AdminEvent> getResultStream() {
if (!predicates.isEmpty()) { if (!predicates.isEmpty()) {
cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); 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<AdminEventEntity> query = em.createQuery(cq); TypedQuery<AdminEventEntity> query = em.createQuery(cq);

View file

@ -48,6 +48,7 @@ public class JpaEventQuery implements EventQuery {
private final ArrayList<Predicate> predicates; private final ArrayList<Predicate> predicates;
private Integer firstResult; private Integer firstResult;
private Integer maxResults; private Integer maxResults;
private boolean orderByDescTime = true;
public JpaEventQuery(EntityManager em) { public JpaEventQuery(EntityManager em) {
this.em = em; this.em = em;
@ -116,13 +117,29 @@ public class JpaEventQuery implements EventQuery {
return this; return this;
} }
@Override
public EventQuery orderByDescTime() {
orderByDescTime = true;
return this;
}
@Override
public EventQuery orderByAscTime() {
orderByDescTime = false;
return this;
}
@Override @Override
public Stream<Event> getResultStream() { public Stream<Event> getResultStream() {
if (!predicates.isEmpty()) { if (!predicates.isEmpty()) {
cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); 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<EventEntity> query = em.createQuery(cq); TypedQuery<EventEntity> query = em.createQuery(cq);

View file

@ -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.IN;
import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.LE; 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.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.QueryParameters.Order.DESCENDING;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
@ -42,6 +43,7 @@ public class MapAdminEventQuery implements AdminEventQuery {
private Integer firstResult; private Integer firstResult;
private Integer maxResults; private Integer maxResults;
private QueryParameters.Order order = DESCENDING;
private DefaultModelCriteria<AdminEvent> mcb = criteria(); private DefaultModelCriteria<AdminEvent> mcb = criteria();
private final Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> resultProducer; private final Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> resultProducer;
@ -121,12 +123,24 @@ public class MapAdminEventQuery implements AdminEventQuery {
return this; return this;
} }
@Override
public AdminEventQuery orderByDescTime() {
order = DESCENDING;
return this;
}
@Override
public AdminEventQuery orderByAscTime() {
order = ASCENDING;
return this;
}
@Override @Override
public Stream<AdminEvent> getResultStream() { public Stream<AdminEvent> getResultStream() {
return resultProducer.apply(QueryParameters.withCriteria(mcb) return resultProducer.apply(QueryParameters.withCriteria(mcb)
.offset(firstResult) .offset(firstResult)
.limit(maxResults) .limit(maxResults)
.orderBy(SearchableFields.TIMESTAMP, DESCENDING) .orderBy(SearchableFields.TIMESTAMP, order)
); );
} }
} }

View file

@ -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.GE;
import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.IN; 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.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.QueryParameters.Order.DESCENDING;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
@ -40,6 +41,7 @@ public class MapAuthEventQuery implements EventQuery {
private Integer firstResult; private Integer firstResult;
private Integer maxResults; private Integer maxResults;
private QueryParameters.Order order = DESCENDING;
private DefaultModelCriteria<Event> mcb = criteria(); private DefaultModelCriteria<Event> mcb = criteria();
private final Function<QueryParameters<Event>, Stream<Event>> resultProducer; private final Function<QueryParameters<Event>, Stream<Event>> resultProducer;
@ -101,11 +103,23 @@ public class MapAuthEventQuery implements EventQuery {
return this; return this;
} }
@Override
public EventQuery orderByDescTime() {
order = DESCENDING;
return this;
}
@Override
public EventQuery orderByAscTime() {
order = ASCENDING;
return this;
}
@Override @Override
public Stream<Event> getResultStream() { public Stream<Event> getResultStream() {
return resultProducer.apply(QueryParameters.withCriteria(mcb) return resultProducer.apply(QueryParameters.withCriteria(mcb)
.offset(firstResult) .offset(firstResult)
.limit(maxResults) .limit(maxResults)
.orderBy(SearchableFields.TIMESTAMP, DESCENDING)); .orderBy(SearchableFields.TIMESTAMP, order));
} }
} }

View file

@ -90,6 +90,20 @@ public interface EventQuery {
*/ */
EventQuery maxResults(int max); EventQuery maxResults(int max);
/**
* Order the result by descending time
*
* @return <code>this</code> for method chaining
*/
EventQuery orderByDescTime();
/**
* Order the result by ascending time
*
* @return <code>this</code> for method chaining
*/
EventQuery orderByAscTime();
/** /**
* @deprecated Use {@link #getResultStream() getResultStream} instead. * @deprecated Use {@link #getResultStream() getResultStream} instead.
*/ */

View file

@ -127,6 +127,20 @@ public interface AdminEventQuery {
*/ */
AdminEventQuery maxResults(int max); AdminEventQuery maxResults(int max);
/**
* Order the result by descending time
*
* @return <code>this</code> for method chaining
*/
AdminEventQuery orderByDescTime();
/**
* Order the result by ascending time
*
* @return <code>this</code> for method chaining
*/
AdminEventQuery orderByAscTime();
/** /**
* Executes the query and returns the results * Executes the query and returns the results
* @deprecated Use {@link #getResultStream() getResultStream} instead. * @deprecated Use {@link #getResultStream() getResultStream} instead.

View file

@ -77,7 +77,7 @@ public class ProtectionService {
KeycloakSession keycloakSession = authorization.getKeycloakSession(); KeycloakSession keycloakSession = authorization.getKeycloakSession();
UserModel serviceAccount = keycloakSession.users().getServiceAccount(client); UserModel serviceAccount = keycloakSession.users().getServiceAccount(client);
AdminEventBuilder adminEvent = new AdminEventBuilder(realm, new AdminAuth(realm, identity.getAccessToken(), serviceAccount, client), keycloakSession, clientConnection); 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") @Path("/permission")

View file

@ -55,7 +55,7 @@ public class AdminEventBuilder {
this.listeners = new HashMap<>(); this.listeners = new HashMap<>();
updateStore(session); updateStore(session);
addListeners(session); addListeners(session);
realm(realm);
authRealm(auth.getRealm()); authRealm(auth.getRealm());
authClient(auth.getClient()); authClient(auth.getClient());
authUser(auth.getUser()); authUser(auth.getUser());

View file

@ -143,7 +143,7 @@ public class RealmAdminResource {
this.auth = auth; this.auth = auth;
this.realm = realm; this.realm = realm;
this.tokenManager = tokenManager; 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()); logger.debug("updating realm events config: " + realm.getName());
new RealmManager(session).updateRealmEventsConfig(rep, realm); 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) .resourcePath(session.getContext().getUri()).representation(rep)
// refresh the builder to consider old and new config // refresh the builder to consider old and new config
.refreshRealmEventsConfig(session) .refreshRealmEventsConfig(session)

View file

@ -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))); 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<AdminEventRepresentation> 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) { private void checkUpdateRealmEventsConfigEvent(int size) {
List<AdminEventRepresentation> events = events(); List<AdminEventRepresentation> events = events();
assertThat(events.size(), is(equalTo(size))); assertThat(events.size(), is(equalTo(size)));

View file

@ -152,6 +152,30 @@ public class LoginEventsTest extends AbstractEventTest {
assertTrue(realm.getEvents(null, null, null, null, null, null, 0, 1000).size() >= 110); 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<EventRepresentation> 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 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 event cleanup is 15 minutes (900 seconds). I don't have time to figure out

View file

@ -16,32 +16,30 @@
*/ */
package org.keycloak.testsuite.model.events; 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.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
/** import org.junit.Test;
* import org.keycloak.common.ClientConnection;
* @author hmlnarik 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) @RequireProvider(EventStoreProvider.class)
public class AdminEventQueryTest extends KeycloakModelTest { public class AdminEventQueryTest extends KeycloakModelTest {
@ -56,128 +54,71 @@ public class AdminEventQueryTest extends KeycloakModelTest {
@Override @Override
public void cleanEnvironment(KeycloakSession s) { public void cleanEnvironment(KeycloakSession s) {
EventStoreProvider eventStore = s.getProvider(EventStoreProvider.class);
eventStore.clearAdmin(s.realms().getRealm(realmId));
s.realms().removeRealm(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 @Test
public void testQuery() { public void testQuery() {
withRealm(realmId, (session, realm) -> { withRealm(realmId, (session, realm) -> {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.onEvent(createAuthEventForUser(realm,"u1")); eventStore.onEvent(createClientEvent(realm, OperationType.CREATE), false);
eventStore.onEvent(createAuthEventForUser(realm,"u2")); eventStore.onEvent(createClientEvent(realm, OperationType.UPDATE), false);
eventStore.onEvent(createAuthEventForUser(realm,"u3")); eventStore.onEvent(createClientEvent(realm, OperationType.DELETE), false);
eventStore.onEvent(createAuthEventForUser(realm,"u4")); eventStore.onEvent(createClientEvent(realm, OperationType.CREATE), false);
return null;
return realm.getId();
}); });
withRealm(realmId, (session, realm) -> { withRealm(realmId, (session, realm) -> {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
assertThat(eventStore.createQuery() assertThat(eventStore.createAdminQuery()
.firstResult(2) .firstResult(2)
.getResultStream() .getResultStream()
.collect(Collectors.counting()), .collect(Collectors.counting()),
is(2L) is(2L));
);
return null; return null;
}); });
} }
@Test @Test
@RequireProvider(value = EventStoreProvider.class, only = "map") public void testQueryOrder() {
public void testEventExpiration() {
withRealm(realmId, (session, realm) -> { withRealm(realmId, (session, realm) -> {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); 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<AdminEvent> 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 List<AdminEvent> adminEventsDesc = eventStore.createAdminQuery()
realm.setEventsExpiration(5); .orderByDescTime()
Event e = createAuthEventForUser(realm, "u1"); .getResultStream()
eventStore.onEvent(e); .collect(Collectors.toList());
assertThat(adminEventsDesc.size(), is(2));
// Set expiration to 1000 seconds assertThat(adminEventsDesc.get(0).getOperationType(), is(OperationType.DELETE));
realm.setEventsExpiration(1000); assertThat(adminEventsDesc.get(1).getOperationType(), is(OperationType.CREATE));
e = createAuthEventForUser(realm, "u2");
eventStore.onEvent(e);
return null; return null;
}); });
Time.setOffset(10);
try {
withRealm(realmId, (session, realm) -> {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
Set<Event> 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 private AdminEvent createClientEvent(RealmModel realm, OperationType operation) {
@RequireProvider(value = EventStoreProvider.class, only = "map") return new AdminEventBuilder(realm, new DummyAuth(realm), null, DummyClientConnection.DUMMY_CONNECTION)
public void testEventsClearedOnRealmRemoval() { .resource(ResourceType.CLIENT).operation(operation).getEvent();
// 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<KeycloakSession>) 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 class DummyClientConnection implements ClientConnection {
private static DummyClientConnection DUMMY_CONNECTION = new DummyClientConnection(); private static final AdminEventQueryTest.DummyClientConnection DUMMY_CONNECTION =
new AdminEventQueryTest.DummyClientConnection();
@Override @Override
public String getRemoteAddr() { 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";
}
};
}
}
} }

View file

@ -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<Event> 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<Event> 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<Event> 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<KeycloakSession>) 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;
}
}
}