Introduce map event store with CHM implementation

Closes #11189
This commit is contained in:
Michal Hajas 2022-04-20 15:45:18 +02:00 committed by Hynek Mlnařík
parent 3ff3aeba29
commit 0bda7e6038
30 changed files with 1279 additions and 58 deletions

View file

@ -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

View file

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

View file

@ -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<String, String> 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);
}
}

View file

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

View file

@ -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<AdminEvent> mcb = criteria();
private final DefaultModelCriteria<AdminEvent> criteria = criteria();
private final Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> resultProducer;
public MapAdminEventQuery(Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> 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<AdminEvent> 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)
);
}
}

View file

@ -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<String, String> getDetails();
void setDetails(Map<String, String> details);
}

View file

@ -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<Event> mcb = criteria();
private final DefaultModelCriteria<Event> criteria = criteria();
private final Function<QueryParameters<Event>, Stream<Event>> resultProducer;
public MapAuthEventQuery(Function<QueryParameters<Event>, Stream<Event>> 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<Event> 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));
}
}

View file

@ -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<MapAuthEventEntity, Event> authEventsTX;
private final MapKeycloakTransaction<MapAdminEventEntity, AdminEvent> adminEventsTX;
public MapEventStoreProvider(KeycloakSession session, MapStorage<MapAuthEventEntity, Event> loginEventsStore, MapStorage<MapAdminEventEntity, AdminEvent> 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<QueryParameters<Event>, Stream<MapAuthEventEntity>>) 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.<Event>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.<Event>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.<Event>criteria()
.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.LE,
Time.currentTimeMillis())));
adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.<AdminEvent>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<QueryParameters<AdminEvent>, Stream<MapAdminEventEntity>>) 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.<AdminEvent>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.<AdminEvent>criteria()
.compare(AdminEvent.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(AdminEvent.SearchableFields.TIME, ModelCriteriaBuilder.Operator.LT, olderThan)
));
}
@Override
public void close() {
}
}

View file

@ -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<EventStoreProvider>, 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);
}
}

View file

@ -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<String, Class<?>> 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<?>, Class<?>> ENTITY_TO_MODEL_TYPE = MODEL_TO_ENTITY_TYPE.entrySet().stream().collect(Collectors.toMap(Entry::getValue, Entry::getKey));

View file

@ -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<String, StringKeyConverter> KEY_CONVERTERS = new HashMap<>();

View file

@ -43,6 +43,7 @@ class CriteriaOperator {
private static final Predicate<Object> ALWAYS_FALSE = o -> false;
private static final Predicate<Object> 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<Object> 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();
};

View file

@ -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<SearchableModelField<UserLoginFailureModel>, UpdatePredicatesFunc<Object, MapUserLoginFailureEntity, UserLoginFailureModel>> USER_LOGIN_FAILURE_PREDICATES = basePredicates(UserLoginFailureModel.SearchableFields.ID);
public static final Map<SearchableModelField<UserModel>, UpdatePredicatesFunc<Object, MapUserEntity, UserModel>> USER_PREDICATES = basePredicates(UserModel.SearchableFields.ID);
public static final Map<SearchableModelField<UserSessionModel>, UpdatePredicatesFunc<Object, MapUserSessionEntity, UserSessionModel>> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID);
public static final Map<SearchableModelField<Event>, UpdatePredicatesFunc<Object, MapAuthEventEntity, Event>> AUTH_EVENTS_PREDICATES = basePredicates(Event.SearchableFields.ID);
public static final Map<SearchableModelField<AdminEvent>, UpdatePredicatesFunc<Object, MapAdminEventEntity, AdminEvent>> ADMIN_EVENTS_PREDICATES = basePredicates(AdminEvent.SearchableFields.ID);
@SuppressWarnings("unchecked")
private static final Map<Class<?>, 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 <K, V extends AbstractEntity, M, L extends Comparable<L>> void put(

View file

@ -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

View file

@ -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<Event> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<Event> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<Event> CLIENT_ID = new SearchableModelField<>("clientId", String.class);
public static final SearchableModelField<Event> USER_ID = new SearchableModelField<>("userId", String.class);
public static final SearchableModelField<Event> TIME = new SearchableModelField<>("time", Long.class);
public static final SearchableModelField<Event> EXPIRATION = new SearchableModelField<>("expiration", Long.class);
public static final SearchableModelField<Event> IP_ADDRESS = new SearchableModelField<>("ipAddress", String.class);
public static final SearchableModelField<Event> EVENT_TYPE = new SearchableModelField<>("eventType", EventType.class);
}
private String id;
private long time;

View file

@ -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.

View file

@ -18,31 +18,86 @@
package org.keycloak.events;
import org.keycloak.events.admin.AdminEventQuery;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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);
}

View file

@ -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

View file

@ -17,11 +17,27 @@
package org.keycloak.events.admin;
import org.keycloak.storage.SearchableModelField;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AdminEvent {
public static class SearchableFields {
public static final SearchableModelField<AdminEvent> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<AdminEvent> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<AdminEvent> TIME = new SearchableModelField<>("time", Long.class);
public static final SearchableModelField<AdminEvent> EXPIRATION = new SearchableModelField<>("expiration", Long.class);
public static final SearchableModelField<AdminEvent> AUTH_REALM_ID = new SearchableModelField<>("authRealmId", String.class);
public static final SearchableModelField<AdminEvent> AUTH_CLIENT_ID = new SearchableModelField<>("authClientId", String.class);
public static final SearchableModelField<AdminEvent> AUTH_USER_ID = new SearchableModelField<>("authUserId", String.class);
public static final SearchableModelField<AdminEvent> AUTH_IP_ADDRESS = new SearchableModelField<>("authIpAddress", String.class);
public static final SearchableModelField<AdminEvent> OPERATION_TYPE = new SearchableModelField<>("operationType", OperationType.class);
public static final SearchableModelField<AdminEvent> RESOURCE_TYPE = new SearchableModelField<>("resourceType", ResourceType.class);
public static final SearchableModelField<AdminEvent> RESOURCE_PATH = new SearchableModelField<>("resourcePath", String.class);
}
private String id;
private long time;

View file

@ -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);
}
/**

View file

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

View file

@ -880,6 +880,7 @@
<keycloak.userSession.provider>map</keycloak.userSession.provider>
<keycloak.loginFailure.provider>map</keycloak.loginFailure.provider>
<keycloak.authorization.provider>map</keycloak.authorization.provider>
<keycloak.eventsStore.provider>map</keycloak.eventsStore.provider>
<keycloak.authorizationCache.enabled>false</keycloak.authorizationCache.enabled>
<keycloak.realmCache.enabled>false</keycloak.realmCache.enabled>
<keycloak.userCache.enabled>false</keycloak.userCache.enabled>

View file

@ -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<? extends Provider> providerClass, String requiredId) {
String providerId = testingClient.server()
.fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(providerClass).getId());
return Objects.equals(providerId, "\"" + requiredId + "\"");
}
protected boolean isRealmCacheEnabled() {

View file

@ -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<RealmRepresentation> 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() {

View file

@ -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"));

View file

@ -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}"
}
}
},

View file

@ -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<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";

View file

@ -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)

View file

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

View file

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