Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public void eventReceived(ClusterEvent event) {
if (realm == null) {
if (fedEvent.isRemoved()) {
logger.debugf("Realm with id %s not found when handling user storage removal event, it may have been deleted already", realmId);
// The realm was removed meanwhile, still cancel the periodic sync tasks of the removed provider
refreshScheduledTasks(session, fedEvent.getStorageProvider(), true);
return;
}
throw new RuntimeException("Failed to execute session task. Realm with id " + realmId + " not found.");
Expand Down Expand Up @@ -79,17 +81,23 @@ public void onEvent(ProviderEvent event) {
runJobInTransaction(sessionFactory, session -> {
RealmModel realm = session.realms().getRealm(realmId);
if (realm == null) {
if (removed && model != null) {
// The realm was removed meanwhile (e.g. in the same transaction that fired this event),
// still cancel the periodic sync tasks of the removed provider
refreshScheduledTasks(session, model, true);
notifyStoreSyncClusterUpdate(session, realmId, model, true);
}
return;
}
session.getContext().setRealm(realm);

if (model != null) {
refreshScheduledTasks(session, model, removed);
notifyStoreSyncClusterUpdate(session, realm, model, removed);
notifyStoreSyncClusterUpdate(session, realmId, model, removed);
} else {
getUserStorageProvidersStream(realm).forEachOrdered(fedProvider -> {
refreshScheduledTasks(session, fedProvider, removed);
notifyStoreSyncClusterUpdate(session, realm, fedProvider, removed);
notifyStoreSyncClusterUpdate(session, realmId, fedProvider, removed);
});
}
});
Expand Down Expand Up @@ -122,7 +130,7 @@ private void scheduleTask(KeycloakSession session, UserStorageProviderModel prov
}

// Ensure all cluster nodes are notified
private void notifyStoreSyncClusterUpdate(KeycloakSession session, RealmModel realm, UserStorageProviderModel provider, boolean removed) {
private void notifyStoreSyncClusterUpdate(KeycloakSession session, String realmId, UserStorageProviderModel provider, boolean removed) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
UserStorageProviderFactory<?> factory = (UserStorageProviderFactory<?>) sessionFactory.getProviderFactory(UserStorageProvider.class, provider.getProviderId());

Expand All @@ -133,7 +141,7 @@ private void notifyStoreSyncClusterUpdate(KeycloakSession session, RealmModel re
ClusterProvider cp = session.getProvider(ClusterProvider.class);

if (cp != null) {
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realmId, provider);
cp.notify(USER_STORAGE_TASK_KEY, event, true);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,9 @@ public void preRemove(RealmModel realm) {

consumeEnabledStorageProvidersWithTimeout(realm, UserStorageProvider.class,
provider -> provider.preRemove(realm));

realm.getComponentsStream(realm.getId(), UserStorageProvider.class.getName())
.forEach(component -> notifyStorageProviderRemoved(realm, component));
}

@Override
Expand Down Expand Up @@ -1149,6 +1152,11 @@ public void preRemove(RealmModel realm, ComponentModel component) {
if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
localStorage().preRemove(realm, component);
if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, component);

notifyStorageProviderRemoved(realm, component);
}

private void notifyStorageProviderRemoved(RealmModel realm, ComponentModel component) {
// enlistAfterCompletion(..) as we need to ensure that the realm is updated with the final settings
session.getTransactionManager().enlistAfterCompletion(new AbstractKeycloakTransaction() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ private UserStorageProviderModel getStorageModel(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();

if (realm == null) {
// The realm was removed, cancel the scheduled task to avoid running it again on the next period
cancel(session);
throw new ModelIllegalStateException("Realm with id " + realmId + " not found");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,23 @@

package org.keycloak.storage;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.storage.federated.UserFederatedStorageProvider;

import org.junit.Test;

Expand All @@ -40,6 +54,128 @@ public class UserStorageManagerTest {

private static final RuntimeException FAILURE = new RuntimeException("simulated storage failure");

@Test
public void realmRemovalNotifiesAfterCommitWithoutRepeatingComponentCleanup() {
assertRealmRemovalNotifications(true);
}

@Test
public void realmRemovalDoesNotNotifyAfterRollback() {
assertRealmRemovalNotifications(false);
}

private void assertRealmRemovalNotifications(boolean commit) {
List<KeycloakTransaction> afterCompletion = new ArrayList<>();
List<StoreSyncEvent> events = new ArrayList<>();
AtomicInteger localRealmRemovals = new AtomicInteger();
AtomicInteger federatedRealmRemovals = new AtomicInteger();
AtomicInteger externalRealmRemovals = new AtomicInteger();
ComponentModel first = new UserStorageProviderModel();
first.setId("first");
ComponentModel second = new UserStorageProviderModel();
second.setId("second");

RealmModel realm = stub(RealmModel.class, (proxy, method, args) -> switch (method.getName()) {
case "getId" -> "realm";
case "getComponentsStream" -> {
assertThat(args[0], is("realm"));
assertThat(args[1], is(UserStorageProvider.class.getName()));
yield Stream.of(first, second);
}
default -> throw new AssertionError("Unexpected realm call: " + method);
});
UserProvider localStorage = stub(UserProvider.class, (proxy, method, args) -> {
assertThat(method.getName(), is("preRemove"));
assertThat(args.length, is(1));
assertSame(realm, args[0]);
localRealmRemovals.incrementAndGet();
return null;
});
UserFederatedStorageProvider federatedStorage = stub(UserFederatedStorageProvider.class, (proxy, method, args) -> {
assertThat(method.getName(), is("preRemove"));
assertThat(args.length, is(1));
assertSame(realm, args[0]);
federatedRealmRemovals.incrementAndGet();
return null;
});
KeycloakTransactionManager transactionManager = stub(KeycloakTransactionManager.class, (proxy, method, args) -> {
assertThat(method.getName(), is("enlistAfterCompletion"));
KeycloakTransaction transaction = (KeycloakTransaction) args[0];
transaction.begin();
afterCompletion.add(transaction);
return null;
});
KeycloakSessionFactory sessionFactory = stub(KeycloakSessionFactory.class, (proxy, method, args) -> {
assertThat(method.getName(), is("publish"));
events.add((StoreSyncEvent) args[0]);
return null;
});
KeycloakSession session = stub(KeycloakSession.class, (proxy, method, args) -> switch (method.getName()) {
case "getTransactionManager" -> transactionManager;
case "getKeycloakSessionFactory" -> sessionFactory;
case "getProvider" -> {
assertSame(UserFederatedStorageProvider.class, args[0]);
yield federatedStorage;
}
default -> throw new AssertionError("Unexpected session call: " + method);
});
UserStorageProvider externalStorage = new UserStorageProvider() {
@Override
public void preRemove(RealmModel removedRealm) {
assertSame(realm, removedRealm);
externalRealmRemovals.incrementAndGet();
}

@Override
public void close() {
}
};
UserStorageManager manager = new UserStorageManager(session) {
@Override
protected UserProvider localStorage() {
return localStorage;
}

@Override
protected <T> void consumeEnabledStorageProvidersWithTimeout(RealmModel removedRealm, Class<T> capabilityInterface,
Consumer<T> consumer) {
consumer.accept(capabilityInterface.cast(externalStorage));
}
};

manager.preRemove(realm);

assertThat(localRealmRemovals.get(), is(1));
assertThat(federatedRealmRemovals.get(), is(1));
assertThat(externalRealmRemovals.get(), is(1));
assertThat(events, empty());
assertThat(afterCompletion.size(), is(2));

afterCompletion.forEach(transaction -> {
if (commit) {
transaction.commit();
} else {
transaction.rollback();
}
});

if (commit) {
assertThat(events.size(), is(2));
assertSame(first, events.get(0).getModel());
assertSame(second, events.get(1).getModel());
events.forEach(event -> {
assertSame(realm, event.getRealm());
assertThat(event.getRemoved(), is(true));
});
} else {
assertThat(events, empty());
}
}

private static <T> T stub(Class<T> type, InvocationHandler handler) {
return type.cast(Proxy.newProxyInstance(UserStorageManagerTest.class.getClassLoader(), new Class<?>[] { type }, handler));
}

@Test
public void queryWithGracefulDegradationPropagatesFailureForLocalStorage() {
// the local/federated provider does not implement UserStorageProvider
Expand Down
Loading
Loading