From 0bda7e6038dc85cd0ef45a9f9abedcd48dc8f9ab Mon Sep 17 00:00:00 2001 From: Michal Hajas Date: Wed, 20 Apr 2022 15:45:18 +0200 Subject: [PATCH] Introduce map event store with CHM implementation Closes #11189 --- .../events/jpa/JpaEventStoreProvider.java | 19 +- .../models/map/common/ExpirableEntity.java | 47 +++++ .../models/map/events/EventUtils.java | 113 +++++++++++ .../map/events/MapAdminEventEntity.java | 83 ++++++++ .../models/map/events/MapAdminEventQuery.java | 144 ++++++++++++++ .../models/map/events/MapAuthEventEntity.java | 79 ++++++++ .../models/map/events/MapAuthEventQuery.java | 123 ++++++++++++ .../map/events/MapEventStoreProvider.java | 184 ++++++++++++++++++ .../events/MapEventStoreProviderFactory.java | 107 ++++++++++ .../models/map/storage/ModelEntityUtil.java | 12 ++ ...ncurrentHashMapStorageProviderFactory.java | 6 + .../map/storage/chm/CriteriaOperator.java | 27 +-- .../map/storage/chm/MapFieldPredicates.java | 27 +++ ....keycloak.events.EventStoreProviderFactory | 18 ++ .../main/java/org/keycloak/events/Event.java | 13 ++ .../java/org/keycloak/events/EventQuery.java | 49 ++++- .../keycloak/events/EventStoreProvider.java | 67 ++++++- .../org/keycloak/events/EventStoreSpi.java | 4 +- .../org/keycloak/events/admin/AdminEvent.java | 16 ++ .../resources/admin/RealmAdminResource.java | 4 +- .../rest/TestingResourceProvider.java | 12 +- .../integration-arquillian/tests/base/pom.xml | 1 + .../testsuite/AbstractKeycloakTest.java | 11 +- .../testsuite/events/AbstractEventsTest.java | 9 + .../events/EventStoreProviderTest.java | 3 + .../resources/META-INF/keycloak-server.json | 8 + .../model/events/AdminEventQueryTest.java | 135 +++++++++++-- .../model/parameters/HotRodMapStorage.java | 6 +- .../model/parameters/LdapMapStorage.java | 5 +- .../testsuite/model/parameters/Map.java | 5 +- 30 files changed, 1279 insertions(+), 58 deletions(-) create mode 100644 model/map/src/main/java/org/keycloak/models/map/common/ExpirableEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/EventUtils.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.events.EventStoreProviderFactory diff --git a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java index 8a577083d4..a9ebfbc739 100755 --- a/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java +++ b/model/jpa/src/main/java/org/keycloak/events/jpa/JpaEventStoreProvider.java @@ -30,6 +30,7 @@ import org.keycloak.events.admin.AdminEventQuery; import org.keycloak.events.admin.AuthDetails; import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; @@ -70,13 +71,13 @@ public class JpaEventStoreProvider implements EventStoreProvider { } @Override - public void clear(String realmId) { - em.createQuery("delete from EventEntity where realmId = :realmId").setParameter("realmId", realmId).executeUpdate(); + public void clear(RealmModel realm) { + em.createQuery("delete from EventEntity where realmId = :realmId").setParameter("realmId", realm.getId()).executeUpdate(); } @Override - public void clear(String realmId, long olderThan) { - em.createQuery("delete from EventEntity where realmId = :realmId and time < :time").setParameter("realmId", realmId).setParameter("time", olderThan).executeUpdate(); + public void clear(RealmModel realm, long olderThan) { + em.createQuery("delete from EventEntity where realmId = :realmId and time < :time").setParameter("realmId", realm.getId()).setParameter("time", olderThan).executeUpdate(); } @Override @@ -105,7 +106,7 @@ public class JpaEventStoreProvider implements EventStoreProvider { session.realms().getRealmsStream().forEach(realm -> { if (realm.isEventsEnabled() && realm.getEventsExpiration() > 0) { long olderThan = Time.currentTimeMillis() - realm.getEventsExpiration() * 1000; - clear(realm.getId(), olderThan); + clear(realm, olderThan); } }); } @@ -127,13 +128,13 @@ public class JpaEventStoreProvider implements EventStoreProvider { } @Override - public void clearAdmin(String realmId) { - em.createQuery("delete from AdminEventEntity where realmId = :realmId").setParameter("realmId", realmId).executeUpdate(); + public void clearAdmin(RealmModel realm) { + em.createQuery("delete from AdminEventEntity where realmId = :realmId").setParameter("realmId", realm.getId()).executeUpdate(); } @Override - public void clearAdmin(String realmId, long olderThan) { - em.createQuery("delete from AdminEventEntity where realmId = :realmId and time < :time").setParameter("realmId", realmId).setParameter("time", olderThan).executeUpdate(); + public void clearAdmin(RealmModel realm, long olderThan) { + em.createQuery("delete from AdminEventEntity where realmId = :realmId and time < :time").setParameter("realmId", realm.getId()).setParameter("time", olderThan).executeUpdate(); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/common/ExpirableEntity.java b/model/map/src/main/java/org/keycloak/models/map/common/ExpirableEntity.java new file mode 100644 index 0000000000..dca034f13b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/ExpirableEntity.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 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.models.map.common; + +/** + * This interface provides a way for marking entities that can expire. For example, user sessions are valid only + * for certain amount of time. After that time the entities can be removed from storage/omitted from query results. + * + * Presence of expired entities in the storage should be transparent to layers above the physical one. This can be + * achieved in more ways. Ideal solution is when expired entities never reach Keycloak codebase, however, this may + * not be possible for all storage implementations, therefore, we need to double-check entities validity before they + * reach logical layer, for example, before we turn entity into model. + * + * Implementation of actual removal of the entities from the storage is responsibility of each storage individually. + * + */ +public interface ExpirableEntity extends AbstractEntity { + + /** + * Returns a point in the time (timestamp in milliseconds since The Epoch) when this entity expires. + * + * @return a timestamp in milliseconds since The Epoch or {@code null} if this entity never expires. + */ + Long getExpiration(); + + /** + * Sets a point in the time (timestamp in milliseconds since The Epoch) when this entity expires. + * + * @param expiration a timestamp in milliseconds since The Epoch or {@code null} if this entity never expires. + */ + void setExpiration(Long expiration); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/events/EventUtils.java b/model/map/src/main/java/org/keycloak/models/map/events/EventUtils.java new file mode 100644 index 0000000000..5e1a522549 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/EventUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.events.Event; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AuthDetails; +import org.keycloak.models.KeycloakSession; + +import java.util.Collections; +import java.util.Map; + +public class EventUtils { + public static Event entityToModel(MapAuthEventEntity eventEntity) { + Event event = new Event(); + event.setId(eventEntity.getId()); + event.setTime(eventEntity.getTime()); + event.setType(eventEntity.getType()); + event.setRealmId(eventEntity.getRealmId()); + event.setClientId(eventEntity.getClientId()); + event.setUserId(eventEntity.getUserId()); + event.setSessionId(eventEntity.getSessionId()); + event.setIpAddress(eventEntity.getIpAddress()); + event.setError(eventEntity.getError()); + + Map details = eventEntity.getDetails(); + event.setDetails(details == null ? Collections.emptyMap() : details); + + return event; + } + + public static AdminEvent entityToModel(MapAdminEventEntity adminEventEntity) { + AdminEvent adminEvent = new AdminEvent(); + adminEvent.setId(adminEventEntity.getId()); + adminEvent.setTime(adminEventEntity.getTime()); + adminEvent.setRealmId(adminEventEntity.getRealmId()); + setAuthDetails(adminEvent, adminEventEntity); + adminEvent.setOperationType(adminEventEntity.getOperationType()); + adminEvent.setResourceTypeAsString(adminEventEntity.getResourceType()); + adminEvent.setResourcePath(adminEventEntity.getResourcePath()); + adminEvent.setError(adminEventEntity.getError()); + + if(adminEventEntity.getRepresentation() != null) { + adminEvent.setRepresentation(adminEventEntity.getRepresentation()); + } + return adminEvent; + + } + + public static MapAdminEventEntity modelToEntity(AdminEvent adminEvent, boolean includeRepresentation) { + MapAdminEventEntity mapAdminEvent = new MapAdminEventEntityImpl(); + mapAdminEvent.setId(adminEvent.getId()); + mapAdminEvent.setTime(adminEvent.getTime()); + mapAdminEvent.setRealmId(adminEvent.getRealmId()); + setAuthDetails(mapAdminEvent, adminEvent.getAuthDetails()); + mapAdminEvent.setOperationType(adminEvent.getOperationType()); + mapAdminEvent.setResourceType(adminEvent.getResourceTypeAsString()); + mapAdminEvent.setResourcePath(adminEvent.getResourcePath()); + mapAdminEvent.setError(adminEvent.getError()); + + if(includeRepresentation) { + mapAdminEvent.setRepresentation(adminEvent.getRepresentation()); + } + return mapAdminEvent; + } + + public static MapAuthEventEntity modelToEntity(Event event) { + MapAuthEventEntity eventEntity = new MapAuthEventEntityImpl(); + eventEntity.setId(event.getId()); + eventEntity.setTime(event.getTime()); + eventEntity.setType(event.getType()); + eventEntity.setRealmId(event.getRealmId()); + eventEntity.setClientId(event.getClientId()); + eventEntity.setUserId(event.getUserId()); + eventEntity.setSessionId(event.getSessionId()); + eventEntity.setIpAddress(event.getIpAddress()); + eventEntity.setError(event.getError()); + eventEntity.setDetails(event.getDetails()); + return eventEntity; + } + + private static void setAuthDetails(MapAdminEventEntity adminEventEntity, AuthDetails authDetails) { + if (authDetails == null) return; + adminEventEntity.setAuthRealmId(authDetails.getRealmId()); + adminEventEntity.setAuthClientId(authDetails.getClientId()); + adminEventEntity.setAuthUserId(authDetails.getUserId()); + adminEventEntity.setAuthIpAddress(authDetails.getIpAddress()); + } + + private static void setAuthDetails(AdminEvent adminEvent, MapAdminEventEntity adminEventEntity) { + AuthDetails authDetails = new AuthDetails(); + authDetails.setRealmId(adminEventEntity.getAuthRealmId()); + authDetails.setClientId(adminEventEntity.getAuthClientId()); + authDetails.setUserId(adminEventEntity.getAuthUserId()); + authDetails.setIpAddress(adminEventEntity.getAuthIpAddress()); + adminEvent.setAuthDetails(authDetails); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventEntity.java b/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventEntity.java new file mode 100644 index 0000000000..52528f79b3 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventEntity.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.UpdatableEntity; + +@GenerateEntityImplementations( + inherits = "org.keycloak.models.map.events.MapAdminEventEntity.AbstractAdminEventEntity" +) +@DeepCloner.Root +public interface MapAdminEventEntity extends UpdatableEntity, AbstractEntity, ExpirableEntity { + + public abstract class AbstractAdminEventEntity extends UpdatableEntity.Impl implements MapAdminEventEntity { + + private String id; + + @Override + public String getId() { + return this.id; + } + + @Override + public void setId(String id) { + if (this.id != null) throw new IllegalStateException("Id cannot be changed"); + this.id = id; + this.updated |= id != null; + } + + } + + Long getTime(); + void setTime(Long time); + + String getRealmId(); + void setRealmId(String realmId); + + OperationType getOperationType(); + void setOperationType(OperationType operationType); + + String getResourcePath(); + void setResourcePath(String resourcePath); + + String getRepresentation(); + void setRepresentation(String representation); + + String getError(); + void setError(String error); + + String getResourceType(); + void setResourceType(String resourceType); + + String getAuthRealmId(); + void setAuthRealmId(String realmId); + + String getAuthClientId(); + void setAuthClientId(String clientId); + + String getAuthUserId(); + void setAuthUserId(String userId); + + String getAuthIpAddress(); + void setAuthIpAddress(String ipAddress); +} \ No newline at end of file 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 new file mode 100644 index 0000000000..619df31750 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAdminEventQuery.java @@ -0,0 +1,144 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.common.util.Time; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEvent.SearchableFields; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.events.admin.OperationType; +import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.map.common.TimeAdapter; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; + +import java.util.Arrays; +import java.util.Date; +import java.util.function.Function; +import java.util.stream.Stream; + +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.ModelCriteriaBuilder.Operator.LIKE; +import static org.keycloak.models.map.storage.QueryParameters.Order.DESCENDING; +import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; + +public class MapAdminEventQuery implements AdminEventQuery { + + private Integer firstResult; + private Integer maxResults; + private DefaultModelCriteria mcb = criteria(); + private final DefaultModelCriteria criteria = criteria(); + private final Function, Stream> resultProducer; + + public MapAdminEventQuery(Function, Stream> resultProducer) { + this.resultProducer = resultProducer; + } + + @Override + public AdminEventQuery realm(String realmId) { + mcb = mcb.compare(SearchableFields.REALM_ID, EQ, realmId); + return this; + } + + @Override + public AdminEventQuery authRealm(String realmId) { + mcb = mcb.compare(SearchableFields.AUTH_REALM_ID, EQ, realmId); + return this; + } + + @Override + public AdminEventQuery authClient(String clientId) { + mcb = mcb.compare(SearchableFields.AUTH_CLIENT_ID, EQ, clientId); + return this; + } + + @Override + public AdminEventQuery authUser(String userId) { + mcb = mcb.compare(SearchableFields.AUTH_USER_ID, EQ, userId); + return this; + } + + @Override + public AdminEventQuery authIpAddress(String ipAddress) { + mcb = mcb.compare(SearchableFields.AUTH_IP_ADDRESS, EQ, ipAddress); + return this; + } + + @Override + public AdminEventQuery operation(OperationType... operations) { + mcb = mcb.compare(SearchableFields.OPERATION_TYPE, IN, Arrays.stream(operations)); + return this; + } + + @Override + public AdminEventQuery resourceType(ResourceType... resourceTypes) { + mcb = mcb.compare(SearchableFields.RESOURCE_TYPE, EQ, Arrays.stream(resourceTypes)); + return this; + } + + @Override + public AdminEventQuery resourcePath(String resourcePath) { + mcb = mcb.compare(SearchableFields.RESOURCE_PATH, LIKE, resourcePath.replace('*', '%')); + return this; + } + + @Override + public AdminEventQuery fromTime(Date fromTime) { + mcb = mcb.compare(SearchableFields.TIME, GE, fromTime.getTime()); + return this; + } + + @Override + public AdminEventQuery toTime(Date toTime) { + mcb = mcb.compare(SearchableFields.TIME, LE, toTime.getTime()); + return this; + } + + @Override + public AdminEventQuery firstResult(int first) { + firstResult = first; + return this; + } + + @Override + public AdminEventQuery maxResults(int max) { + maxResults = max; + return this; + } + + @Override + public Stream getResultStream() { + // Add expiration condition to not load expired events + mcb = mcb.and( + criteria.or( + criteria.compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.NOT_EXISTS), + criteria.compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.GT, + Time.currentTimeMillis()) + )); + + return resultProducer.apply(QueryParameters.withCriteria(mcb) + .offset(firstResult) + .limit(maxResults) + .orderBy(SearchableFields.TIME, DESCENDING) + ); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventEntity.java b/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventEntity.java new file mode 100644 index 0000000000..459ed4b8cb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventEntity.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.events.EventType; +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.UpdatableEntity; + +import java.util.Map; + +@GenerateEntityImplementations( + inherits = "org.keycloak.models.map.events.MapAuthEventEntity.AbstractAuthEventEntity" +) +@DeepCloner.Root +public interface MapAuthEventEntity extends UpdatableEntity, AbstractEntity, ExpirableEntity { + + public abstract class AbstractAuthEventEntity extends UpdatableEntity.Impl implements MapAuthEventEntity { + + private String id; + + @Override + public String getId() { + return this.id; + } + + @Override + public void setId(String id) { + if (this.id != null) throw new IllegalStateException("Id cannot be changed"); + this.id = id; + this.updated |= id != null; + } + + } + + Long getTime(); + void setTime(Long time); + + EventType getType(); + void setType(EventType type); + + String getRealmId(); + void setRealmId(String realmId); + + String getClientId(); + void setClientId(String clientId); + + String getUserId(); + void setUserId(String userId); + + String getSessionId(); + void setSessionId(String sessionId); + + String getIpAddress(); + void setIpAddress(String ipAddress); + + String getError(); + void setError(String error); + + Map getDetails(); + void setDetails(Map details); +} 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 new file mode 100644 index 0000000000..fb021ab611 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapAuthEventQuery.java @@ -0,0 +1,123 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.common.util.Time; +import org.keycloak.events.Event; +import org.keycloak.events.Event.SearchableFields; +import org.keycloak.events.EventQuery; +import org.keycloak.events.EventType; +import org.keycloak.models.map.common.TimeAdapter; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; + +import java.util.Arrays; +import java.util.Date; +import java.util.function.Function; +import java.util.stream.Stream; + +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.DESCENDING; +import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; + +public class MapAuthEventQuery implements EventQuery { + + private Integer firstResult; + private Integer maxResults; + private DefaultModelCriteria mcb = criteria(); + private final DefaultModelCriteria criteria = criteria(); + private final Function, Stream> resultProducer; + + public MapAuthEventQuery(Function, Stream> resultProducer) { + this.resultProducer = resultProducer; + } + + @Override + public EventQuery type(EventType... types) { + mcb = mcb.compare(SearchableFields.EVENT_TYPE, IN, Arrays.asList(types)); + return this; + } + + @Override + public EventQuery realm(String realmId) { + mcb = mcb.compare(SearchableFields.REALM_ID, EQ, realmId); + return this; + } + + @Override + public EventQuery client(String clientId) { + mcb = mcb.compare(SearchableFields.CLIENT_ID, EQ, clientId); + return this; + } + + @Override + public EventQuery user(String userId) { + mcb = mcb.compare(SearchableFields.USER_ID, EQ, userId); + return this; + } + + @Override + public EventQuery fromDate(Date fromDate) { + mcb = mcb.compare(SearchableFields.TIME, GE, fromDate.getTime()); + return this; + } + + @Override + public EventQuery toDate(Date toDate) { + mcb = mcb.compare(SearchableFields.TIME, LE, toDate.getTime()); + return this; + } + + @Override + public EventQuery ipAddress(String ipAddress) { + mcb = mcb.compare(SearchableFields.IP_ADDRESS, EQ, ipAddress); + return this; + } + + @Override + public EventQuery firstResult(int firstResult) { + this.firstResult = firstResult; + return this; + } + + @Override + public EventQuery maxResults(int max) { + this.maxResults = max; + return this; + } + + @Override + public Stream getResultStream() { + // Add expiration condition to not load expired events + mcb = mcb.and( + criteria.or( + criteria.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.NOT_EXISTS), + criteria.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.GT, + Time.currentTimeMillis()) + )); + + return resultProducer.apply(QueryParameters.withCriteria(mcb) + .offset(firstResult) + .limit(maxResults) + .orderBy(SearchableFields.TIME, DESCENDING)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java new file mode 100644 index 0000000000..7566327d14 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java @@ -0,0 +1,184 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.events.Event; +import org.keycloak.events.EventQuery; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.TimeAdapter; +import org.keycloak.models.map.group.MapGroupProvider; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; + +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.models.map.events.EventUtils.modelToEntity; + +public class MapEventStoreProvider implements EventStoreProvider { + + private static final Logger LOG = Logger.getLogger(MapEventStoreProvider.class); + private final KeycloakSession session; + private final MapKeycloakTransaction authEventsTX; + private final MapKeycloakTransaction adminEventsTX; + + public MapEventStoreProvider(KeycloakSession session, MapStorage loginEventsStore, MapStorage adminEventsStore) { + this.session = session; + this.authEventsTX = loginEventsStore.createTransaction(session); + this.adminEventsTX = adminEventsStore.createTransaction(session); + + session.getTransactionManager().enlistAfterCompletion(this.authEventsTX); + session.getTransactionManager().enlistAfterCompletion(this.adminEventsTX); + } + + /** LOGIN EVENTS **/ + @Override + public void onEvent(Event event) { + LOG.tracef("onEvent(%s)%s", event, getShortStackTrace()); + String id = event.getId(); + + if (id != null && authEventsTX.read(id) != null) { + throw new ModelDuplicateException("Event already exists: " + id); + } + + MapAuthEventEntity entity = modelToEntity(event); + String realmId = event.getRealmId(); + if (realmId != null) { + RealmModel realm = session.realms().getRealm(realmId); + if (realm != null && realm.getEventsExpiration() > 0) { + entity.setExpiration(Time.currentTimeMillis() + (realm.getEventsExpiration() * 1000)); + } + } + + authEventsTX.create(entity); + } + + private boolean filterExpired(ExpirableEntity event) { + Long expiration = event.getExpiration(); + // Check if entity is expired + if (expiration != null && expiration <= Time.currentTimeMillis()) { + // Remove entity + authEventsTX.delete(event.getId()); + + return false; // Do not include entity in the resulting stream + } + + return true; // Entity is not expired + } + + @Override + public EventQuery createQuery() { + LOG.tracef("createQuery()%s", getShortStackTrace()); + return new MapAuthEventQuery(((Function, Stream>) authEventsTX::read) + .andThen(s -> s.filter(this::filterExpired).map(EventUtils::entityToModel))); + } + + @Override + public void clear() { + LOG.tracef("clear()%s", getShortStackTrace()); + authEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria())); + } + + @Override + public void clear(RealmModel realm) { + LOG.tracef("clear(%s)%s", realm, getShortStackTrace()); + authEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(Event.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()))); + } + + @Override + public void clear(RealmModel realm, long olderThan) { + LOG.tracef("clear(%s, %d)%s", realm, olderThan, getShortStackTrace()); + authEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(Event.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(Event.SearchableFields.TIME, ModelCriteriaBuilder.Operator.LT, olderThan) + )); + } + + @Override + public void clearExpiredEvents() { + LOG.tracef("clearExpiredEvents()%s", getShortStackTrace()); + + authEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.LE, + Time.currentTimeMillis()))); + + adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.LE, + Time.currentTimeMillis()))); + } + + /** ADMIN EVENTS **/ + + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + LOG.tracef("clear(%s, %s)%s", event, includeRepresentation, getShortStackTrace()); + String id = event.getId(); + if (id != null && authEventsTX.read(id) != null) { + throw new ModelDuplicateException("Event already exists: " + id); + } + + adminEventsTX.create(modelToEntity(event, includeRepresentation)); + } + + @Override + public AdminEventQuery createAdminQuery() { + LOG.tracef("createAdminQuery()%s", getShortStackTrace()); + return new MapAdminEventQuery(((Function, Stream>) adminEventsTX::read) + .andThen(s -> s.filter(this::filterExpired).map(EventUtils::entityToModel))); + } + + @Override + public void clearAdmin() { + LOG.tracef("clearAdmin()%s", getShortStackTrace()); + adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria())); + } + + @Override + public void clearAdmin(RealmModel realm) { + LOG.tracef("clear(%s)%s", realm, getShortStackTrace()); + adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(AdminEvent.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()))); + } + + @Override + public void clearAdmin(RealmModel realm, long olderThan) { + LOG.tracef("clearAdmin(%s, %d)%s", realm, olderThan, getShortStackTrace()); + adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.criteria() + .compare(AdminEvent.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId()) + .compare(AdminEvent.SearchableFields.TIME, ModelCriteriaBuilder.Operator.LT, olderThan) + )); + } + + @Override + public void close() { + + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java new file mode 100644 index 0000000000..2256f04da0 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java @@ -0,0 +1,107 @@ +/* + * Copyright 2022 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.models.map.events; + +import org.keycloak.Config; +import org.keycloak.common.Profile; +import org.keycloak.component.AmphibianProviderFactory; +import org.keycloak.events.Event; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.EventStoreProviderFactory; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.MapStorageSpi; +import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.InvalidationHandler; + +import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProviderObjectType.REALM_BEFORE_REMOVE; +import static org.keycloak.models.map.common.AbstractMapProviderFactory.uniqueCounter; +import static org.keycloak.models.utils.KeycloakModelUtils.getComponentFactory; + +public class MapEventStoreProviderFactory implements AmphibianProviderFactory, EnvironmentDependentProviderFactory, EventStoreProviderFactory, InvalidationHandler { + + public static final String PROVIDER_ID = AbstractMapProviderFactory.PROVIDER_ID; + private Config.Scope storageConfigScopeAdminEvents; + private Config.Scope storageConfigScopeLoginEvents; + private final String uniqueKey = getClass().getName() + uniqueCounter.incrementAndGet(); + + + @Override + public void init(Config.Scope config) { + storageConfigScopeAdminEvents = config.scope(AbstractMapProviderFactory.CONFIG_STORAGE + "-admin-events"); + storageConfigScopeLoginEvents = config.scope(AbstractMapProviderFactory.CONFIG_STORAGE + "-auth-events"); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public EventStoreProvider create(KeycloakSession session) { + MapEventStoreProvider provider = session.getAttribute(uniqueKey, MapEventStoreProvider.class); + if (provider != null) return provider; + + MapStorageProviderFactory storageProviderFactoryAe = (MapStorageProviderFactory) getComponentFactory(session.getKeycloakSessionFactory(), + MapStorageProvider.class, storageConfigScopeAdminEvents, MapStorageSpi.NAME); + final MapStorageProvider factoryAe = storageProviderFactoryAe.create(session); + MapStorage adminEventsStore = factoryAe.getStorage(AdminEvent.class); + + MapStorageProviderFactory storageProviderFactoryLe = (MapStorageProviderFactory) getComponentFactory(session.getKeycloakSessionFactory(), + MapStorageProvider.class, storageConfigScopeLoginEvents, MapStorageSpi.NAME); + final MapStorageProvider factoryLe = storageProviderFactoryLe.create(session); + MapStorage loginEventsStore = factoryLe.getStorage(Event.class); + + provider = new MapEventStoreProvider(session, loginEventsStore, adminEventsStore); + session.setAttribute(uniqueKey, provider); + return provider; + } + + @Override + public void invalidate(KeycloakSession session, InvalidationHandler.InvalidableObjectType type, Object... params) { + if (type == REALM_BEFORE_REMOVE) { + create(session).clear((RealmModel) params[0]); + create(session).clearAdmin((RealmModel) params[0]); + } + } + + @Override + public void close() { + AmphibianProviderFactory.super.close(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "Event provider"; + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java b/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java index 5705c80bde..2b64d622af 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java @@ -20,6 +20,8 @@ import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.events.Event; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -38,6 +40,8 @@ import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.clientscope.MapClientScopeEntity; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.events.MapAdminEventEntity; +import org.keycloak.models.map.events.MapAuthEventEntity; import org.keycloak.models.map.group.MapGroupEntity; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.realm.MapRealmEntity; @@ -77,6 +81,10 @@ public class ModelEntityUtil { MODEL_TO_NAME.put(ResourceServer.class, "authz-resource-servers"); MODEL_TO_NAME.put(Resource.class, "authz-resources"); MODEL_TO_NAME.put(org.keycloak.authorization.model.Scope.class, "authz-scopes"); + + // events + MODEL_TO_NAME.put(AdminEvent.class, "admin-events"); + MODEL_TO_NAME.put(Event.class, "auth-events"); } private static final Map> NAME_TO_MODEL = MODEL_TO_NAME.entrySet().stream().collect(Collectors.toMap(Entry::getValue, Entry::getKey)); @@ -99,6 +107,10 @@ public class ModelEntityUtil { MODEL_TO_ENTITY_TYPE.put(ResourceServer.class, MapResourceServerEntity.class); MODEL_TO_ENTITY_TYPE.put(Resource.class, MapResourceEntity.class); MODEL_TO_ENTITY_TYPE.put(org.keycloak.authorization.model.Scope.class, MapScopeEntity.class); + + // events + MODEL_TO_ENTITY_TYPE.put(AdminEvent.class, MapAdminEventEntity.class); + MODEL_TO_ENTITY_TYPE.put(Event.class, MapAuthEventEntity.class); } private static final Map, Class> ENTITY_TO_MODEL_TYPE = MODEL_TO_ENTITY_TYPE.entrySet().stream().collect(Collectors.toMap(Entry::getValue, Entry::getKey)); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java index fd35bb085c..3f7301fa58 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java @@ -45,6 +45,10 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.Serialization; import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.events.MapAdminEventEntity; +import org.keycloak.models.map.events.MapAdminEventEntityImpl; +import org.keycloak.models.map.events.MapAuthEventEntity; +import org.keycloak.models.map.events.MapAuthEventEntityImpl; import org.keycloak.models.map.group.MapGroupEntityImpl; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntityImpl; @@ -137,6 +141,8 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide .constructor(MapUserLoginFailureEntity.class, MapUserLoginFailureEntityImpl::new) .constructor(MapUserSessionEntity.class, MapUserSessionEntityImpl::new) .constructor(MapAuthenticatedClientSessionEntity.class, MapAuthenticatedClientSessionEntityImpl::new) + .constructor(MapAuthEventEntity.class, MapAuthEventEntityImpl::new) + .constructor(MapAdminEventEntity.class, MapAdminEventEntityImpl::new) .build(); private static final Map KEY_CONVERTERS = new HashMap<>(); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java index 7025107d2b..95e7465a6b 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java @@ -43,6 +43,7 @@ class CriteriaOperator { private static final Predicate ALWAYS_FALSE = o -> false; private static final Predicate ALWAYS_TRUE = o -> true; + private static final Pattern LIKE_PATTERN_DELIMITER = Pattern.compile("%+"); static { OPERATORS.put(Operator.EQ, CriteriaOperator::eq); @@ -194,15 +195,7 @@ class CriteriaOperator { return ALWAYS_TRUE; } - boolean anyBeginning = sValue.startsWith("%"); - boolean anyEnd = sValue.endsWith("%"); - - Pattern pValue = Pattern.compile( - (anyBeginning ? ".*" : "") - + Pattern.quote(sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0))) - + (anyEnd ? ".*" : ""), - Pattern.DOTALL - ); + Pattern pValue = Pattern.compile(quoteRegex(sValue), Pattern.DOTALL); return o -> { return o instanceof String && pValue.matcher((String) o).matches(); }; @@ -210,6 +203,12 @@ class CriteriaOperator { return ALWAYS_FALSE; } + private static String quoteRegex(String pattern) { + return LIKE_PATTERN_DELIMITER.splitAsStream(pattern).map(Pattern::quote) + .collect(Collectors.joining(".*")) + + (pattern.endsWith("%") ? ".*" : ""); + } + public static Predicate ilike(Object[] value) { Object value0 = getFirstArrayElement(value); if (value0 instanceof String) { @@ -219,15 +218,7 @@ class CriteriaOperator { return ALWAYS_TRUE; } - boolean anyBeginning = sValue.startsWith("%"); - boolean anyEnd = sValue.endsWith("%"); - - Pattern pValue = Pattern.compile( - (anyBeginning ? ".*" : "") - + Pattern.quote(sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0))) - + (anyEnd ? ".*" : ""), - Pattern.CASE_INSENSITIVE + Pattern.DOTALL - ); + Pattern pValue = Pattern.compile(quoteRegex(sValue), Pattern.CASE_INSENSITIVE + Pattern.DOTALL); return o -> { return o instanceof String && pValue.matcher((String) o).matches(); }; diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index 055afee512..78ffae64d3 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -21,6 +21,8 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; +import org.keycloak.events.Event; +import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -39,6 +41,8 @@ import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.clientscope.MapClientScopeEntity; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.events.MapAdminEventEntity; +import org.keycloak.models.map.events.MapAuthEventEntity; import org.keycloak.models.map.group.MapGroupEntity; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.realm.MapRealmEntity; @@ -92,6 +96,8 @@ public class MapFieldPredicates { public static final Map, UpdatePredicatesFunc> USER_LOGIN_FAILURE_PREDICATES = basePredicates(UserLoginFailureModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc> USER_PREDICATES = basePredicates(UserModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc> AUTH_EVENTS_PREDICATES = basePredicates(Event.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc> ADMIN_EVENTS_PREDICATES = basePredicates(AdminEvent.SearchableFields.ID); @SuppressWarnings("unchecked") private static final Map, Map> PREDICATES = new HashMap<>(); @@ -201,6 +207,25 @@ public class MapFieldPredicates { put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.REALM_ID, MapUserLoginFailureEntity::getRealmId); put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.USER_ID, MapUserLoginFailureEntity::getUserId); + + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.REALM_ID, MapAuthEventEntity::getRealmId); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.CLIENT_ID, MapAuthEventEntity::getClientId); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.USER_ID, MapAuthEventEntity::getUserId); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.TIME, MapAuthEventEntity::getTime); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.EXPIRATION, MapAuthEventEntity::getExpiration); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.IP_ADDRESS, MapAuthEventEntity::getIpAddress); + put(AUTH_EVENTS_PREDICATES, Event.SearchableFields.EVENT_TYPE, MapAuthEventEntity::getType); + + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.REALM_ID, MapAdminEventEntity::getRealmId); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.TIME, MapAdminEventEntity::getTime); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.EXPIRATION, MapAdminEventEntity::getExpiration); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.AUTH_REALM_ID, MapAdminEventEntity::getAuthRealmId); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.AUTH_CLIENT_ID, MapAdminEventEntity::getAuthClientId); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.AUTH_USER_ID, MapAdminEventEntity::getAuthUserId); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.AUTH_IP_ADDRESS, MapAdminEventEntity::getAuthIpAddress); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.OPERATION_TYPE, MapAdminEventEntity::getOperationType); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_TYPE, MapAdminEventEntity::getResourceType); + put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_PATH, MapAdminEventEntity::getResourcePath); } static { @@ -219,6 +244,8 @@ public class MapFieldPredicates { PREDICATES.put(UserSessionModel.class, USER_SESSION_PREDICATES); PREDICATES.put(AuthenticatedClientSessionModel.class, CLIENT_SESSION_PREDICATES); PREDICATES.put(UserLoginFailureModel.class, USER_LOGIN_FAILURE_PREDICATES); + PREDICATES.put(Event.class, AUTH_EVENTS_PREDICATES); + PREDICATES.put(AdminEvent.class, ADMIN_EVENTS_PREDICATES); } private static > void put( diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.events.EventStoreProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.events.EventStoreProviderFactory new file mode 100644 index 0000000000..c9ed94d221 --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.events.EventStoreProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2016 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. +# + +org.keycloak.models.map.events.MapEventStoreProviderFactory \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/events/Event.java b/server-spi-private/src/main/java/org/keycloak/events/Event.java index 5da17d138d..d9e5defaeb 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/Event.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Event.java @@ -17,6 +17,8 @@ package org.keycloak.events; +import org.keycloak.storage.SearchableModelField; + import java.util.HashMap; import java.util.Map; @@ -25,6 +27,17 @@ import java.util.Map; */ public class Event { + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField CLIENT_ID = new SearchableModelField<>("clientId", String.class); + public static final SearchableModelField USER_ID = new SearchableModelField<>("userId", String.class); + public static final SearchableModelField TIME = new SearchableModelField<>("time", Long.class); + public static final SearchableModelField EXPIRATION = new SearchableModelField<>("expiration", Long.class); + public static final SearchableModelField IP_ADDRESS = new SearchableModelField<>("ipAddress", String.class); + public static final SearchableModelField EVENT_TYPE = new SearchableModelField<>("eventType", EventType.class); + } + private String id; private long time; 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 4591a01122..62063072ce 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 @@ -27,23 +27,68 @@ import java.util.stream.Stream; */ public interface EventQuery { + /** + * Search events with given types + * @param types requested types + * @return this object for method chaining + */ EventQuery type(EventType... types); + /** + * Search events within realm + * @param realmId id of realm + * @return this object for method chaining + */ EventQuery realm(String realmId); + /** + * Search events for only one client + * @param clientId id of client + * @return this object for method chaining + */ EventQuery client(String clientId); + /** + * Search events for only one user + * @param userId id of user + * @return this object for method chaining + */ EventQuery user(String userId); + /** + * Search events that are newer than {@code fromDate} + * @param fromDate date + * @return this object for method chaining + */ EventQuery fromDate(Date fromDate); + /** + * Search events that are older than {@code toDate} + * @param toDate date + * @return this object for method chaining + */ EventQuery toDate(Date toDate); + /** + * Search events from ipAddress + * @param ipAddress ip + * @return this object for method chaining + */ EventQuery ipAddress(String ipAddress); - EventQuery firstResult(int result); + /** + * Index of the first result to return. + * @param firstResult the index. Ignored if negative. + * @return this object for method chaining + */ + EventQuery firstResult(int firstResult); - EventQuery maxResults(int results); + /** + * Maximum number of results to return. + * @param max a number. Ignored if negative. + * @return this object for method chaining + */ + EventQuery maxResults(int max); /** * @deprecated Use {@link #getResultStream() getResultStream} instead. diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventStoreProvider.java b/server-spi-private/src/main/java/org/keycloak/events/EventStoreProvider.java index f4e82291d4..e4cae20bab 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/EventStoreProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventStoreProvider.java @@ -18,31 +18,86 @@ package org.keycloak.events; import org.keycloak.events.admin.AdminEventQuery; +import org.keycloak.models.RealmModel; /** * @author Stian Thorgersen */ public interface EventStoreProvider extends EventListenerProvider { + /** + * Returns an object representing auth event query of type {@link EventQuery}. + * + * The object is used for collecting requested properties of auth events (e.g. realm, operation, resourceType + * time boundaries, etc.) and contains the {@link EventQuery#getResultStream()} method that returns all + * objects from this store provider that have given properties. + * + * @return a query object + */ EventQuery createQuery(); + /** + * Returns an object representing admin event query of type {@link AdminEventQuery}. + * + * The object is used for collecting requested properties of admin events (e.g. realm, operation, resourceType + * time boundaries, etc.) and contains the {@link AdminEventQuery#getResultStream()} method that returns all + * objects from this store provider that have given properties. + * + * @return a query object + */ AdminEventQuery createAdminQuery(); + /** + * Removes all auth events from this store provider. + * + * @deprecated Unused method. Currently, used only in the testsuite + */ void clear(); - void clear(String realmId); - - void clear(String realmId, long olderThan); + /** + * Removes all auth events for the realm from this store provider. + * @param realm the realm + * + */ + void clear(RealmModel realm); /** - * Clear all expired events in all realms + * Removes all auth events for the realm that are older than {@code olderThan} from this store provider. + * + * @param realm the realm + * @param olderThan point in time in milliseconds + */ + void clear(RealmModel realm, long olderThan); + + /** + * Clears all expired events in all realms + * + * @deprecated This method is problem from the performance perspective. Some storages can provide better way + * for doing this (e.g. entry lifespan in the Infinispan server, etc.). We need to leave solving event expiration + * to each storage provider separately using expiration field on entity level. + * */ void clearExpiredEvents(); + /** + * Removes all admin events from this store provider. + * + * @deprecated Unused method. Currently, used only in the testsuite + */ void clearAdmin(); - void clearAdmin(String realmId); + /** + * Removes all auth events for the realm from this store provider. + * @param realm the realm + */ + void clearAdmin(RealmModel realm); - void clearAdmin(String realmId, long olderThan); + /** + * Removes all auth events for the realm that are older than {@code olderThan} from this store provider. + * + * @param realm the realm + * @param olderThan point in time in milliseconds + */ + void clearAdmin(RealmModel realm, long olderThan); } diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventStoreSpi.java b/server-spi-private/src/main/java/org/keycloak/events/EventStoreSpi.java index ff02a55684..56b1ae5d32 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/EventStoreSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventStoreSpi.java @@ -26,6 +26,8 @@ import org.keycloak.provider.Spi; */ public class EventStoreSpi implements Spi { + public static final String NAME = "eventsStore"; + @Override public boolean isInternal() { return true; @@ -33,7 +35,7 @@ public class EventStoreSpi implements Spi { @Override public String getName() { - return "eventsStore"; + return NAME; } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java index 6c6fb3aff7..74a9ce8b9c 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java +++ b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java @@ -17,11 +17,27 @@ package org.keycloak.events.admin; +import org.keycloak.storage.SearchableModelField; + /** * @author Stian Thorgersen */ public class AdminEvent { + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField TIME = new SearchableModelField<>("time", Long.class); + public static final SearchableModelField EXPIRATION = new SearchableModelField<>("expiration", Long.class); + public static final SearchableModelField AUTH_REALM_ID = new SearchableModelField<>("authRealmId", String.class); + public static final SearchableModelField AUTH_CLIENT_ID = new SearchableModelField<>("authClientId", String.class); + public static final SearchableModelField AUTH_USER_ID = new SearchableModelField<>("authUserId", String.class); + public static final SearchableModelField AUTH_IP_ADDRESS = new SearchableModelField<>("authIpAddress", String.class); + public static final SearchableModelField OPERATION_TYPE = new SearchableModelField<>("operationType", OperationType.class); + public static final SearchableModelField RESOURCE_TYPE = new SearchableModelField<>("resourceType", ResourceType.class); + public static final SearchableModelField RESOURCE_PATH = new SearchableModelField<>("resourcePath", String.class); + } + private String id; private long time; 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 80fb447105..2564d0fda6 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 @@ -904,7 +904,7 @@ public class RealmAdminResource { auth.realm().requireManageEvents(); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clear(realm.getId()); + eventStore.clear(realm); } /** @@ -917,7 +917,7 @@ public class RealmAdminResource { auth.realm().requireManageEvents(); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clearAdmin(realm.getId()); + eventStore.clearAdmin(realm); } /** diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 1acaea95d3..826f36229e 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.rest; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.Config; +import org.keycloak.authorization.policy.evaluation.Realm; import org.keycloak.common.Profile; import org.keycloak.common.util.HtmlUtils; import org.keycloak.common.util.Time; @@ -299,7 +300,9 @@ public class TestingResourceProvider implements RealmResourceProvider { @Produces(MediaType.APPLICATION_JSON) public Response clearEventStore(@QueryParam("realmId") String realmId) { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clear(realmId); + RealmModel realm = session.realms().getRealm(realmId); + + eventStore.clear(realm); return Response.noContent().build(); } @@ -422,7 +425,9 @@ public class TestingResourceProvider implements RealmResourceProvider { @Produces(MediaType.APPLICATION_JSON) public Response clearAdminEventStore(@QueryParam("realmId") String realmId) { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clearAdmin(realmId); + RealmModel realm = session.realms().getRealm(realmId); + + eventStore.clearAdmin(realm); return Response.noContent().build(); } @@ -431,7 +436,8 @@ public class TestingResourceProvider implements RealmResourceProvider { @Produces(MediaType.APPLICATION_JSON) public Response clearAdminEventStore(@QueryParam("realmId") String realmId, @QueryParam("olderThan") long olderThan) { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - eventStore.clearAdmin(realmId, olderThan); + RealmModel realm = session.realms().getRealm(realmId); + eventStore.clearAdmin(realm, olderThan); return Response.noContent().build(); } diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 38e3fe327a..c8d58bce46 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -880,6 +880,7 @@ map map map + map false false false diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index 1b44f45610..16b32dbefd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -39,6 +39,7 @@ import org.keycloak.common.util.Time; import org.keycloak.models.RealmProvider; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.UserCache; +import org.keycloak.provider.Provider; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; @@ -735,9 +736,13 @@ public abstract class AbstractKeycloakTest { * returns true if realm provider is "jpa" to be able to skip particular tests. */ protected boolean isJpaRealmProvider() { - String realmProvider = testingClient.server() - .fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(RealmProvider.class).getId()); - return Objects.equals(realmProvider, "\"jpa\""); + return keycloakUsingProviderWithId(RealmProvider.class, "jpa"); + } + + protected boolean keycloakUsingProviderWithId(Class providerClass, String requiredId) { + String providerId = testingClient.server() + .fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(providerClass).getId()); + return Objects.equals(providerId, "\"" + requiredId + "\""); } protected boolean isRealmCacheEnabled() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AbstractEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AbstractEventsTest.java index 09fd435f79..a43b2fb7b9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AbstractEventsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AbstractEventsTest.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.events; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.client.resources.TestingResource; +import org.keycloak.testsuite.util.RealmBuilder; import java.util.ArrayList; import java.util.List; @@ -32,6 +33,14 @@ public abstract class AbstractEventsTest extends AbstractKeycloakTest { @Override public void addTestRealms(List testRealms) { + RealmRepresentation rep1 = RealmBuilder.create().name("realmId").build(); + rep1.setId("realmId"); + + RealmRepresentation rep2 = RealmBuilder.create().name("realmId2").build(); + rep2.setId("realmId2"); + + testRealms.add(rep1); + testRealms.add(rep2); } protected TestingResource testing() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java index c0abfb1874..1c2b472ea2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java @@ -20,9 +20,11 @@ package org.keycloak.testsuite.events; import org.apache.commons.lang3.StringUtils; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.keycloak.common.util.Time; +import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory; import org.keycloak.models.utils.KeycloakModelUtils; @@ -209,6 +211,7 @@ public class EventStoreProviderTest extends AbstractEventsTest { @Test public void clearOld() { + Assume.assumeTrue("Map storage event store provider does not support changing expiration of existing events", keycloakUsingProviderWithId(EventStoreProvider.class, "jpa")); testing().onEvent(create(System.currentTimeMillis() - 300000, EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); testing().onEvent(create(System.currentTimeMillis() - 200000, EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); testing().onEvent(create(System.currentTimeMillis(), EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index e6b6f12d1e..20b9c3f0e5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -25,6 +25,14 @@ "provider": "${keycloak.eventsStore.provider:jpa}", "jpa": { "max-detail-length": "${keycloak.eventsStore.maxDetailLength:1000}" + }, + "map": { + "storage-admin-events": { + "provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}" + }, + "storage-auth-events": { + "provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}" + } } }, 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 b939b8d95b..b2b216e2ff 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 @@ -17,15 +17,25 @@ 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; /** @@ -35,6 +45,19 @@ import static org.hamcrest.Matchers.is; @RequireProvider(EventStoreProvider.class) public class AdminEventQueryTest 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() { @@ -44,28 +67,118 @@ public class AdminEventQueryTest extends KeycloakModelTest { }); } + 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() { - inRolledBackTransaction(null, (session, t) -> { + withRealm(realmId, (session, realm) -> { EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); - RealmModel realm = session.realms().createRealm("realm"); - ClientConnection cc = new DummyClientConnection(); - eventStore.onEvent(new EventBuilder(realm, null, cc).event(EventType.LOGIN).user("u1").getEvent()); - eventStore.onEvent(new EventBuilder(realm, null, cc).event(EventType.LOGIN).user("u2").getEvent()); - eventStore.onEvent(new EventBuilder(realm, null, cc).event(EventType.LOGIN).user("u3").getEvent()); - eventStore.onEvent(new EventBuilder(realm, null, cc).event(EventType.LOGIN).user("u4").getEvent()); + 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) + .firstResult(2) + .getResultStream() + .collect(Collectors.counting()), + is(2L) ); + + 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"; diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java index 2bd57da6af..b6442289df 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableSet; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.keycloak.authorization.store.StoreFactorySpi; +import org.keycloak.events.EventStoreSpi; import org.keycloak.models.DeploymentStateSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; @@ -28,6 +29,7 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderF import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; import org.keycloak.models.map.client.MapClientProviderFactory; import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory; +import org.keycloak.models.map.events.MapEventStoreProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionSpi; @@ -84,7 +86,9 @@ public class HotRodMapStorage extends KeycloakModelParameters { .spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID).config("storage-user-sessions.provider", HotRodMapStorageProviderFactory.PROVIDER_ID) .config("storage-client-sessions.provider", HotRodMapStorageProviderFactory.PROVIDER_ID) .spi(UserLoginFailureSpi.NAME).provider(MapUserLoginFailureProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) - .spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); + .spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID).config("storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .config("storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); cf.spi(MapStorageSpi.NAME) .provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java index 2110cea6d0..0b199e6eaa 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.keycloak.authorization.store.StoreFactorySpi; +import org.keycloak.events.EventStoreSpi; import org.keycloak.models.DeploymentStateSpi; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelDuplicateException; @@ -101,7 +102,9 @@ public class LdapMapStorage extends KeycloakModelParameters { .spi(UserSessionSpi.NAME).config("map.storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(UserLoginFailureSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("authorizationPersister").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) - .spi("authenticationSessions").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); + .spi("authenticationSessions").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(EventStoreSpi.NAME).config("map.storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java index 97eb6f1433..75c258ac18 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java @@ -17,12 +17,14 @@ package org.keycloak.testsuite.model.parameters; import org.keycloak.authorization.store.StoreFactorySpi; +import org.keycloak.events.EventStoreSpi; import org.keycloak.models.DeploymentStateSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.dblock.NoLockingDBLockProviderFactory; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory; import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; +import org.keycloak.models.map.events.MapEventStoreProviderFactory; import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory; import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; import org.keycloak.sessions.AuthenticationSessionSpi; @@ -33,7 +35,6 @@ import org.keycloak.models.map.group.MapGroupProviderFactory; import org.keycloak.models.map.realm.MapRealmProviderFactory; import org.keycloak.models.map.role.MapRoleProviderFactory; import org.keycloak.models.map.deploymentState.MapDeploymentStateProviderFactory; -import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.storage.MapStorageSpi; import org.keycloak.models.map.user.MapUserProviderFactory; import org.keycloak.provider.ProviderFactory; @@ -67,6 +68,7 @@ public class Map extends KeycloakModelParameters { .add(MapUserSessionProviderFactory.class) .add(MapUserLoginFailureProviderFactory.class) .add(NoLockingDBLockProviderFactory.class) + .add(MapEventStoreProviderFactory.class) .build(); public Map() { @@ -87,6 +89,7 @@ public class Map extends KeycloakModelParameters { .spi(UserSessionSpi.NAME).defaultProvider(MapUserSessionProviderFactory.PROVIDER_ID) .spi(UserLoginFailureSpi.NAME).defaultProvider(MapUserLoginFailureProviderFactory.PROVIDER_ID) .spi("dblock").defaultProvider(NoLockingDBLockProviderFactory.PROVIDER_ID) + .spi(EventStoreSpi.NAME).defaultProvider(MapEventStoreProviderFactory.PROVIDER_ID) ; } }