introduce expiration option for admin events
This commit is contained in:
parent
a6137b9b86
commit
19d69169b1
11 changed files with 213 additions and 17 deletions
|
@ -31,14 +31,25 @@ 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.jpa.entities.RealmAttributeEntity;
|
||||
import org.keycloak.models.jpa.entities.RealmAttributes;
|
||||
import org.keycloak.models.jpa.entities.RealmEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.InvalidationHandler;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.TypedQuery;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Root;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -261,4 +272,21 @@ public class JpaEventStoreProvider implements EventStoreProvider {
|
|||
adminEvent.setAuthDetails(authDetails);
|
||||
}
|
||||
|
||||
protected void clearExpiredAdminEvents() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<RealmAttributeEntity> cr = cb.createQuery(RealmAttributeEntity.class);
|
||||
Root<RealmAttributeEntity> root = cr.from(RealmAttributeEntity.class);
|
||||
cr.select(root).where(cb.and(cb.equal(root.get("name"),RealmAttributes.ADMIN_EVENTS_EXPIRATION),cb.greaterThan(root.get("value"),Long.valueOf(0))));
|
||||
Map<Long, List<RealmAttributeEntity>> realms = em.createQuery(cr).getResultStream().collect(Collectors.groupingBy(attribute -> Long.valueOf(attribute.getValue())));
|
||||
|
||||
long current = Time.currentTimeMillis();
|
||||
realms.entrySet().forEach(entry -> {
|
||||
List<String> realmIds = entry.getValue().stream().map(RealmAttributeEntity::getRealm).map(RealmEntity::getId).collect(Collectors.toList());
|
||||
int currentNumDeleted = em.createQuery("delete from AdminEventEntity where realmId in :realmIds and time < :eventTime")
|
||||
.setParameter("realmIds", realmIds)
|
||||
.setParameter("eventTime", current - (Long.valueOf(entry.getKey()) * 1000))
|
||||
.executeUpdate();
|
||||
logger.tracef("Deleted %d admin events for the expiration %d", currentNumDeleted, entry.getKey());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,16 +18,19 @@
|
|||
package org.keycloak.events.jpa;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.EventStoreProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.InvalidationHandler;
|
||||
import org.keycloak.storage.datastore.PeriodicEventInvalidation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class JpaEventStoreProviderFactory implements EventStoreProviderFactory {
|
||||
public class JpaEventStoreProviderFactory implements EventStoreProviderFactory, InvalidationHandler {
|
||||
|
||||
public static final String ID = "jpa";
|
||||
private int maxDetailLength;
|
||||
|
@ -57,4 +60,10 @@ public class JpaEventStoreProviderFactory implements EventStoreProviderFactory {
|
|||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(KeycloakSession session, InvalidableObjectType type, Object... params) {
|
||||
if(type == PeriodicEventInvalidation.JPA_EVENT_STORE) {
|
||||
((JpaEventStoreProvider) session.getProvider(EventStoreProvider.class)).clearExpiredAdminEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,4 +51,6 @@ public interface RealmAttributes {
|
|||
String WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER = "webAuthnPolicyAvoidSameAuthenticatorRegister";
|
||||
String WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS = "webAuthnPolicyAcceptableAaguids";
|
||||
|
||||
String ADMIN_EVENTS_EXPIRATION = "adminEventsExpiration";
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.keycloak.services.scheduled;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.EventStoreProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.InvalidationHandler;
|
||||
import org.keycloak.storage.datastore.PeriodicEventInvalidation;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
public class ClearExpiredAdminEvents implements ScheduledTask {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(ClearExpiredAdminEvents.class);
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
long currentTimeMillis = Time.currentTimeMillis();
|
||||
session.invalidate(PeriodicEventInvalidation.JPA_EVENT_STORE);
|
||||
long took = Time.currentTimeMillis() - currentTimeMillis;
|
||||
logger.debugf("ClearExpiredEvents finished in %d ms", took);
|
||||
}
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ import org.keycloak.models.utils.PostMigrationEvent;
|
|||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
import org.keycloak.services.scheduled.ClearExpiredAdminEvents;
|
||||
import org.keycloak.services.scheduled.ClearExpiredClientInitialAccessTokens;
|
||||
import org.keycloak.services.scheduled.ClearExpiredEvents;
|
||||
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
||||
|
@ -102,6 +103,7 @@ public class LegacyDatastoreProviderFactory implements DatastoreProviderFactory,
|
|||
TimerProvider timer = session.getProvider(TimerProvider.class);
|
||||
if (timer != null) {
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredAdminEvents(), interval), interval, "ClearExpiredAdminEvents");
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens");
|
||||
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, ClearExpiredUserSessions.TASK_NAME);
|
||||
UserStorageSyncManager.bootstrapPeriodic(sessionFactory, timer);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package org.keycloak.storage.datastore;
|
||||
|
||||
import org.keycloak.provider.InvalidationHandler;
|
||||
|
||||
public enum PeriodicEventInvalidation implements InvalidationHandler.InvalidableObjectType {
|
||||
JPA_EVENT_STORE,
|
||||
}
|
|
@ -135,8 +135,16 @@ public class MapEventStoreProvider implements EventStoreProvider {
|
|||
if (id != null && authEventsTX.read(id) != null) {
|
||||
throw new ModelDuplicateException("Event already exists: " + id);
|
||||
}
|
||||
|
||||
adminEventsTX.create(modelToEntity(event, includeRepresentation));
|
||||
String realmId = event.getRealmId();
|
||||
MapAdminEventEntity entity = modelToEntity(event,includeRepresentation);
|
||||
if (realmId != null) {
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
Long expiration = realm.getAttribute("adminEventsExpiration",0L);
|
||||
if (realm != null && expiration > 0) {
|
||||
entity.setExpiration(Time.currentTimeMillis() + (expiration * 1000));
|
||||
}
|
||||
}
|
||||
adminEventsTX.create(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -64,6 +64,7 @@ import org.keycloak.services.resource.RealmResourceProvider;
|
|||
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.datastore.PeriodicEventInvalidation;
|
||||
import org.keycloak.testsuite.components.TestProvider;
|
||||
import org.keycloak.testsuite.components.TestProviderFactory;
|
||||
import org.keycloak.testsuite.components.amphibian.TestAmphibianProvider;
|
||||
|
@ -315,9 +316,11 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response clearExpiredEvents() {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
eventStore.clearExpiredEvents();
|
||||
session.invalidate(PeriodicEventInvalidation.JPA_EVENT_STORE);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Query events
|
||||
* <p>
|
||||
|
|
|
@ -19,10 +19,14 @@ package org.keycloak.testsuite.events;
|
|||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.models.jpa.entities.RealmAttributes;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.AuthDetailsRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
@ -30,7 +34,9 @@ import java.text.SimpleDateFormat;
|
|||
import java.util.Date;
|
||||
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:giriraj.sharma27@gmail.com">Giriraj Sharma</a>
|
||||
|
@ -192,6 +198,52 @@ public class AdminEventStoreProviderTest extends AbstractEventsTest {
|
|||
Assert.assertEquals(2, testing().getAdminEvents(null, null, null, null, null, null, null, null, null, null, null).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expireOld() {
|
||||
Assume.assumeTrue("Map storage event store provider does not support changing expiration of existing events", keycloakUsingProviderWithId(EventStoreProvider.class, "jpa"));
|
||||
testing().onAdminEvent(create(System.currentTimeMillis() - 30000, realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
testing().onAdminEvent(create(System.currentTimeMillis() - 20000, realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
testing().onAdminEvent(create(System.currentTimeMillis(), realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
testing().onAdminEvent(create(System.currentTimeMillis(), realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
testing().onAdminEvent(create(System.currentTimeMillis() - 30000, realmId2, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
testing().onAdminEvent(create(System.currentTimeMillis(), realmId2, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false);
|
||||
|
||||
// Set expiration of events for realmId .
|
||||
RealmRepresentation realm = realmsResouce().realm(REALM_NAME_1).toRepresentation();
|
||||
Map<String, String> attributes = realm.getAttributes();
|
||||
attributes.put(RealmAttributes.ADMIN_EVENTS_EXPIRATION,"10");
|
||||
realm.setAttributes(attributes);
|
||||
realmsResouce().realm(REALM_NAME_1).update(realm);
|
||||
|
||||
// The first 2 events from realmId will be deleted
|
||||
testing().clearExpiredEvents();
|
||||
Assert.assertEquals(4, testing().getAdminEvents(null, null, null, null, null, null, null, null, null, null, null).size());
|
||||
|
||||
// Set expiration of events for realmId2 as well
|
||||
RealmRepresentation realm2 = realmsResouce().realm(REALM_NAME_2).toRepresentation();
|
||||
Map<String, String> attributes2 = realm2.getAttributes();
|
||||
attributes2.put(RealmAttributes.ADMIN_EVENTS_EXPIRATION,"10");
|
||||
realm2.setAttributes(attributes2);
|
||||
realmsResouce().realm(REALM_NAME_2).update(realm2);
|
||||
|
||||
// The first event from realmId2 will be deleted now
|
||||
testing().clearExpiredEvents();
|
||||
Assert.assertEquals(3, testing().getAdminEvents(null, null, null, null, null, null, null, null, null, null, null).size());
|
||||
|
||||
// set time offset to the future. The remaining 2 events from realmId and 1 event from realmId2 should be expired now
|
||||
setTimeOffset(150);
|
||||
testing().clearExpiredEvents();
|
||||
Assert.assertEquals(0, testing().getAdminEvents(REALM_NAME_1, null, null, null, null, null, null, null, null, null, null).size());
|
||||
|
||||
// Revert expirations
|
||||
attributes.put(RealmAttributes.ADMIN_EVENTS_EXPIRATION,"0");
|
||||
realm.setAttributes(attributes);
|
||||
realmsResouce().realm(REALM_NAME_1).update(realm);
|
||||
attributes2.put(RealmAttributes.ADMIN_EVENTS_EXPIRATION,"0");
|
||||
realm2.setAttributes(attributes2);
|
||||
realmsResouce().realm(REALM_NAME_2).update(realm2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCustomResourceTypeEvents() {
|
||||
testing().onAdminEvent(create(realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "my-custom-resource", "error"), false);
|
||||
|
|
|
@ -2438,13 +2438,15 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
|
|||
}
|
||||
});
|
||||
|
||||
module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmEventsConfig, RealmEvents, RealmAdminEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog) {
|
||||
module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmEventsConfig, RealmEvents, RealmAdminEvents, realm, serverInfo, $location, Notifications, TimeUnit, Dialog, Realm) {
|
||||
$scope.realm = realm;
|
||||
|
||||
$scope.eventsConfig = eventsConfig;
|
||||
|
||||
$scope.eventsConfig.expirationUnit = TimeUnit.autoUnit(eventsConfig.eventsExpiration);
|
||||
$scope.eventsConfig.eventsExpiration = TimeUnit.toUnit(eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit);
|
||||
$scope.realm.attributes.adminEventsExpirationUnit = TimeUnit.autoUnit(realm.attributes.adminEventsExpiration);
|
||||
$scope.realm.attributes.adminEventsExpiration = TimeUnit.toUnit(realm.attributes.adminEventsExpiration, $scope.realm.attributes.adminEventsExpirationUnit);
|
||||
|
||||
$scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers);
|
||||
|
||||
|
@ -2460,34 +2462,66 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE
|
|||
'tags': serverInfo.enums['eventType']
|
||||
};
|
||||
|
||||
var oldCopy = angular.copy($scope.eventsConfig);
|
||||
|
||||
$scope.changed = false;
|
||||
|
||||
var oldCopy = angular.copy($scope.eventsConfig);
|
||||
$scope.configChanged = false;
|
||||
$scope.$watch('eventsConfig', function() {
|
||||
if (!angular.equals($scope.eventsConfig, oldCopy)) {
|
||||
$scope.configChanged = true;
|
||||
$scope.changed = true;
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.attributesChanged = false;
|
||||
var oldAttributes = angular.copy($scope.realm.attributes)
|
||||
$scope.$watch('realm.attributes', function() {
|
||||
if($scope.realm.attributes.adminEventsExpiration != oldAttributes.adminEventsExpiration || $scope.realm.attributes.adminEventsExpirationUnit != oldAttributes.adminEventsExpirationUnit)
|
||||
$scope.attributesChanged = true;
|
||||
$scope.changed = true;
|
||||
},true);
|
||||
|
||||
$scope.save = function() {
|
||||
$scope.changed = false;
|
||||
|
||||
var copy = angular.copy($scope.eventsConfig)
|
||||
delete copy['expirationUnit'];
|
||||
|
||||
copy.eventsExpiration = TimeUnit.toSeconds($scope.eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit);
|
||||
|
||||
RealmEventsConfig.update({
|
||||
id : realm.realm
|
||||
}, copy, function () {
|
||||
var successFunction = function(){
|
||||
$location.url("/realms/" + realm.realm + "/events-settings");
|
||||
Notifications.success("Your changes have been saved to the realm.");
|
||||
});
|
||||
};
|
||||
|
||||
var updateAttributes = function (){
|
||||
$scope.attributesChanged = false;
|
||||
var realmCopy = angular.copy($scope.realm)
|
||||
delete realmCopy.attributes['adminEventsExpirationUnit'];
|
||||
realmCopy.attributes.adminEventsExpiration = TimeUnit.toSeconds($scope.realm.attributes.adminEventsExpiration, $scope.realm.attributes.adminEventsExpirationUnit);
|
||||
Realm.update({id: realm.realm},realmCopy,successFunction)
|
||||
};
|
||||
|
||||
if($scope.configChanged) {
|
||||
var copy = angular.copy($scope.eventsConfig)
|
||||
delete copy['expirationUnit'];
|
||||
copy.eventsExpiration = TimeUnit.toSeconds($scope.eventsConfig.eventsExpiration, $scope.eventsConfig.expirationUnit);
|
||||
RealmEventsConfig.update({
|
||||
id: realm.realm
|
||||
}, copy, function () {
|
||||
$scope.configChanged = false;
|
||||
if($scope.attributesChanged){
|
||||
updateAttributes()
|
||||
}else{
|
||||
successFunction()
|
||||
}
|
||||
});
|
||||
} else if($scope.attributesChanged){
|
||||
updateAttributes()
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.eventsConfig = angular.copy(oldCopy);
|
||||
$scope.changed = false;
|
||||
$scope.realm.attributes.adminEventsExpiration = oldAttributes.adminEventsExpiration;
|
||||
$scope.realm.attributes.adminEventsExpirationUnit = oldAttributes.adminEventsExpirationUnit;
|
||||
$scope.attributesChanged = false;
|
||||
};
|
||||
|
||||
$scope.clearEvents = function() {
|
||||
|
|
|
@ -91,7 +91,18 @@
|
|||
<button class="btn btn-danger" type="submit" data-ng-click="clearAdminEvents()" >{{:: 'clear-admin-events' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="eventsConfig.adminEventsEnabled">
|
||||
<label class="col-md-2 control-label" for="expiration">{{:: 'expiration' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'events.expiration.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" data-ng-model="realm.attributes.adminEventsExpiration" id="adminEventsExpiration" name="adminEventsExpiration" min="0"/>
|
||||
<select class="form-control" name="adminEventsExpirationUnit" data-ng-model="realm.attributes.adminEventsExpirationUnit">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group" data-ng-show="access.manageEvents">
|
||||
|
|
Loading…
Reference in a new issue