parent
3ff3aeba29
commit
0bda7e6038
30 changed files with 1279 additions and 58 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
||||
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";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue