diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java index a5b8f46f42..f7d1d55c32 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java @@ -56,6 +56,8 @@ import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.policy.PasswordPolicyManagerProvider; import org.keycloak.policy.PolicyError; import org.keycloak.models.cache.UserCache; +import org.keycloak.storage.DatastoreProvider; +import org.keycloak.storage.LegacyStoreManagers; import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; @@ -179,7 +181,8 @@ public class LDAPStorageProvider implements UserStorageProvider, // We need to avoid having CachedUserModel as cache is upper-layer then LDAP. Hence having CachedUserModel here may cause StackOverflowError if (local instanceof CachedUserModel) { - local = session.userStorageManager().getUserById(realm, local.getId()); + LegacyStoreManagers datastoreProvider = (LegacyStoreManagers) session.getProvider(DatastoreProvider.class); + local = datastoreProvider.userStorageManager().getUserById(realm, local.getId()); existing = userManager.getManagedProxiedUser(local.getId()); if (existing != null) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index c119f7e4b3..5569fe2fff 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -27,9 +27,9 @@ import org.keycloak.models.cache.infinispan.entities.*; import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.DatastoreProvider; +import org.keycloak.storage.LegacyStoreManagers; import org.keycloak.storage.StorageId; import org.keycloak.storage.client.ClientStorageProviderModel; -import org.keycloak.storage.datastore.LegacyDatastoreProvider; import java.util.*; import java.util.stream.Collectors; @@ -121,13 +121,13 @@ public class RealmCacheSession implements CacheRealmProvider { protected boolean clearAll; protected final long startupRevision; - private final LegacyDatastoreProvider datastoreProvider; + private final LegacyStoreManagers datastoreProvider; public RealmCacheSession(RealmCacheManager cache, KeycloakSession session) { this.cache = cache; this.session = session; this.startupRevision = cache.getCurrentCounter(); - this.datastoreProvider = (LegacyDatastoreProvider) session.getProvider(DatastoreProvider.class); + this.datastoreProvider = (LegacyStoreManagers) session.getProvider(DatastoreProvider.class); session.getTransactionManager().enlistPrepare(getPrepareTransaction()); session.getTransactionManager().enlistAfterCompletion(getAfterTransaction()); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index ed0dc22790..fc8f802558 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -53,6 +53,10 @@ import org.keycloak.models.cache.infinispan.stream.InIdentityProviderPredicate; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.storage.CacheableStorageProviderModel; +import org.keycloak.storage.DatastoreProvider; +import org.keycloak.storage.LegacyStoreManagers; +import org.keycloak.storage.OnCreateComponent; +import org.keycloak.storage.OnUpdateComponent; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; @@ -71,7 +75,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ -public class UserCacheSession implements UserCache.Streams { +public class UserCacheSession implements UserCache.Streams, OnCreateComponent, OnUpdateComponent { protected static final Logger logger = Logger.getLogger(UserCacheSession.class); protected UserCacheManager cache; protected KeycloakSession session; @@ -85,11 +89,13 @@ public class UserCacheSession implements UserCache.Streams { protected Set realmInvalidations = new HashSet<>(); protected Set invalidationEvents = new HashSet<>(); // Events to be sent across cluster protected Map managedUsers = new HashMap<>(); + private LegacyStoreManagers datastoreProvider; public UserCacheSession(UserCacheManager cache, KeycloakSession session) { this.cache = cache; this.session = session; this.startupRevision = cache.getCurrentCounter(); + this.datastoreProvider = (LegacyStoreManagers) session.getProvider(DatastoreProvider.class); session.getTransactionManager().enlistAfterCompletion(getTransaction()); } @@ -103,7 +109,7 @@ public class UserCacheSession implements UserCache.Streams { public UserProvider getDelegate() { if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction"); if (delegate != null) return delegate; - delegate = session.userStorageManager(); + delegate = this.datastoreProvider.userStorageManager(); return delegate; } @@ -906,4 +912,17 @@ public class UserCacheSession implements UserCache.Streams { invalidationEvents.add(UserCacheRealmInvalidationEvent.create(realmId)); } + @Override + public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) { + if (getDelegate() instanceof OnUpdateComponent) { + ((OnUpdateComponent) getDelegate()).onUpdate(session, realm, oldModel, newModel); + } + } + + @Override + public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { + if (getDelegate() instanceof OnCreateComponent) { + ((OnCreateComponent) getDelegate()).onCreate(session, realm, model); + } + } } diff --git a/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java new file mode 100644 index 0000000000..94a4f6b391 --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredClientInitialAccessTokens.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 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.keycloak.models.KeycloakSession; +import org.keycloak.timer.ScheduledTask; + +/** + * @author Marek Posolda + */ +public class ClearExpiredClientInitialAccessTokens implements ScheduledTask { + + @Override + public void run(KeycloakSession session) { + session.realms().removeExpiredClientInitialAccess(); + } +} diff --git a/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredEvents.java b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredEvents.java new file mode 100755 index 0000000000..16d70c089d --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredEvents.java @@ -0,0 +1,46 @@ +/* + * 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.models.KeycloakSession; +import org.keycloak.timer.ScheduledTask; + +/** + * @author Stian Thorgersen + */ +public class ClearExpiredEvents implements ScheduledTask { + + protected static final Logger logger = Logger.getLogger(ClearExpiredEvents.class); + + @Override + public void run(KeycloakSession session) { + long currentTimeMillis = Time.currentTimeMillis(); + + EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); + if (eventStore != null) { + eventStore.clearExpiredEvents(); + } + + long took = Time.currentTimeMillis() - currentTimeMillis; + logger.debugf("ClearExpiredEvents finished in %d ms", took); + } + +} diff --git a/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java new file mode 100755 index 0000000000..904dc1f6e3 --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java @@ -0,0 +1,45 @@ +/* + * 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.models.KeycloakSession; +import org.keycloak.timer.ScheduledTask; + +/** + * @author Stian Thorgersen + */ +public class ClearExpiredUserSessions implements ScheduledTask { + + protected static final Logger logger = Logger.getLogger(ClearExpiredUserSessions.class); + + public static final String TASK_NAME = "ClearExpiredUserSessions"; + + @Override + public void run(KeycloakSession session) { + long currentTimeMillis = Time.currentTimeMillis(); + + session.authenticationSessions().removeAllExpired(); + session.sessions().removeAllExpired(); + + long took = Time.currentTimeMillis() - currentTimeMillis; + logger.debugf("ClearExpiredUserSessions finished in %d ms", took); + } + +} diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java similarity index 99% rename from services/src/main/java/org/keycloak/storage/UserStorageManager.java rename to model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java index dc8fe12232..bf5ddd3277 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -17,6 +17,18 @@ package org.keycloak.storage; +import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction; +import static org.keycloak.utils.StreamsUtil.distinctByKey; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + import org.jboss.logging.Logger; import org.keycloak.component.ComponentFactory; import org.keycloak.component.ComponentModel; @@ -39,27 +51,15 @@ import org.keycloak.models.cache.OnUserCache; import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.ComponentUtil; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; -import org.keycloak.services.managers.UserStorageSyncManager; import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; +import org.keycloak.storage.managers.UserStorageSyncManager; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserBulkUpdateProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; - -import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction; -import static org.keycloak.utils.StreamsUtil.distinctByKey; -import static org.keycloak.utils.StreamsUtil.paginatedStream; - /** * @author Bill Burke * @version $Revision: 1 $ diff --git a/model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java similarity index 84% rename from model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java rename to model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java index 01a1227d0e..096225eaad 100644 --- a/model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProvider.java @@ -6,14 +6,18 @@ import org.keycloak.models.GroupProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleProvider; +import org.keycloak.models.UserProvider; import org.keycloak.models.cache.CacheRealmProvider; +import org.keycloak.models.cache.UserCache; import org.keycloak.storage.ClientScopeStorageManager; import org.keycloak.storage.ClientStorageManager; import org.keycloak.storage.DatastoreProvider; import org.keycloak.storage.GroupStorageManager; +import org.keycloak.storage.LegacyStoreManagers; import org.keycloak.storage.RoleStorageManager; +import org.keycloak.storage.UserStorageManager; -public class LegacyDatastoreProvider implements DatastoreProvider { +public class LegacyDatastoreProvider implements DatastoreProvider, LegacyStoreManagers { private final LegacyDatastoreProviderFactory factory; private final KeycloakSession session; @@ -23,11 +27,13 @@ public class LegacyDatastoreProvider implements DatastoreProvider { private GroupProvider groupProvider; private RealmProvider realmProvider; private RoleProvider roleProvider; + private UserProvider userProvider; private ClientScopeStorageManager clientScopeStorageManager; private RoleStorageManager roleStorageManager; private GroupStorageManager groupStorageManager; private ClientStorageManager clientStorageManager; + private UserProvider userStorageManager; public LegacyDatastoreProvider(LegacyDatastoreProviderFactory factory, KeycloakSession session) { this.factory = factory; @@ -66,6 +72,13 @@ public class LegacyDatastoreProvider implements DatastoreProvider { return groupStorageManager; } + public UserProvider userStorageManager() { + if (userStorageManager == null) { + userStorageManager = new UserStorageManager(session); + } + return userStorageManager; + } + private ClientProvider getClientProvider() { // TODO: Extract ClientProvider from CacheRealmProvider and use that instead ClientProvider cache = session.getProvider(CacheRealmProvider.class); @@ -115,6 +128,15 @@ public class LegacyDatastoreProvider implements DatastoreProvider { } } + private UserProvider getUserProvider() { + UserCache cache = session.getProvider(UserCache.class); + if (cache != null) { + return cache; + } else { + return userStorageManager(); + } + } + @Override public ClientProvider clients() { if (clientProvider == null) { @@ -154,4 +176,12 @@ public class LegacyDatastoreProvider implements DatastoreProvider { } return roleProvider; } + + @Override + public UserProvider users() { + if (userProvider == null) { + userProvider = getUserProvider(); + } + return userProvider; + } } diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java new file mode 100644 index 0000000000..6b0a44ab70 --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java @@ -0,0 +1,90 @@ +package org.keycloak.storage.datastore; + +import org.keycloak.Config; +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.utils.PostMigrationEvent; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderEventListener; +import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.services.scheduled.ClearExpiredClientInitialAccessTokens; +import org.keycloak.services.scheduled.ClearExpiredEvents; +import org.keycloak.services.scheduled.ClearExpiredUserSessions; +import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner; +import org.keycloak.services.scheduled.ScheduledTaskRunner; +import org.keycloak.storage.DatastoreProvider; +import org.keycloak.storage.DatastoreProviderFactory; +import org.keycloak.storage.LegacyStoreSyncEvent; +import org.keycloak.timer.TimerProvider; + +public class LegacyDatastoreProviderFactory implements DatastoreProviderFactory, ProviderEventListener { + + private static final String PROVIDER_ID = "legacy"; + private long clientStorageProviderTimeout; + private long roleStorageProviderTimeout; + private Runnable onClose; + + @Override + public DatastoreProvider create(KeycloakSession session) { + return new LegacyDatastoreProvider(this, session); + } + + @Override + public void init(Scope config) { + clientStorageProviderTimeout = Config.scope("client").getLong("storageProviderTimeout", 3000L); + roleStorageProviderTimeout = Config.scope("role").getLong("storageProviderTimeout", 3000L); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + factory.register(this); + onClose = () -> factory.unregister(this); + } + + @Override + public void close() { + onClose.run(); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + public long getClientStorageProviderTimeout() { + return clientStorageProviderTimeout; + } + + public long getRoleStorageProviderTimeout() { + return roleStorageProviderTimeout; + } + + @Override + public void onEvent(ProviderEvent event) { + if (event instanceof PostMigrationEvent) { + setupScheduledTasks(((PostMigrationEvent) event).getFactory()); + } else if (event instanceof LegacyStoreSyncEvent) { + LegacyStoreSyncEvent ev = (LegacyStoreSyncEvent) event; + UserStorageSyncManager.notifyToRefreshPeriodicSyncAll(ev.getSession(), ev.getRealm(), ev.getRemoved()); + } + } + + public static void setupScheduledTasks(final KeycloakSessionFactory sessionFactory) { + long interval = Config.scope("scheduled").getLong("interval", 900L) * 1000; + + KeycloakSession session = sessionFactory.create(); + try { + 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 ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens"); + timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, ClearExpiredUserSessions.TASK_NAME); + UserStorageSyncManager.bootstrapPeriodic(sessionFactory, timer); + } + } finally { + session.close(); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/managers/UserStorageSyncManager.java similarity index 87% rename from services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java rename to model/legacy-private/src/main/java/org/keycloak/storage/managers/UserStorageSyncManager.java index 163722c925..c2a0cd48ea 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/managers/UserStorageSyncManager.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.services.managers; +package org.keycloak.storage.managers; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterEvent; @@ -22,12 +22,12 @@ import org.keycloak.cluster.ClusterListener; import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ExecutionResult; import org.keycloak.common.util.Time; +import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.services.ServicesLogger; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel; @@ -54,7 +54,7 @@ public class UserStorageSyncManager { * @param sessionFactory * @param timer */ - public void bootstrapPeriodic(final KeycloakSessionFactory sessionFactory, final TimerProvider timer) { + public static void bootstrapPeriodic(final KeycloakSessionFactory sessionFactory, final TimerProvider timer) { KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override @@ -80,7 +80,7 @@ public class UserStorageSyncManager { ExecutionResult result; } - public SynchronizationResult syncAllUsers(final KeycloakSessionFactory sessionFactory, final String realmId, final UserStorageProviderModel provider) { + public static SynchronizationResult syncAllUsers(final KeycloakSessionFactory sessionFactory, final String realmId, final UserStorageProviderModel provider) { UserStorageProviderFactory factory = (UserStorageProviderFactory) sessionFactory.getProviderFactory(UserStorageProvider.class, provider.getProviderId()); if (!(factory instanceof ImportSynchronization) || !provider.isImportEnabled() || !provider.isEnabled()) { return SynchronizationResult.ignored(); @@ -121,7 +121,7 @@ public class UserStorageSyncManager { } } - public SynchronizationResult syncChangedUsers(final KeycloakSessionFactory sessionFactory, final String realmId, final UserStorageProviderModel provider) { + public static SynchronizationResult syncChangedUsers(final KeycloakSessionFactory sessionFactory, final String realmId, final UserStorageProviderModel provider) { UserStorageProviderFactory factory = (UserStorageProviderFactory) sessionFactory.getProviderFactory(UserStorageProvider.class, provider.getProviderId()); if (!(factory instanceof ImportSynchronization) || !provider.isImportEnabled() || !provider.isEnabled()) { return SynchronizationResult.ignored(); @@ -164,8 +164,17 @@ public class UserStorageSyncManager { } + public static void notifyToRefreshPeriodicSyncAll(KeycloakSession session, RealmModel realm, boolean removed) { + realm.getUserStorageProvidersStream().forEachOrdered(fedProvider -> + notifyToRefreshPeriodicSync(session, realm, fedProvider, removed)); + } + + public static void notifyToRefreshPeriodicSyncSingle(KeycloakSession session, RealmModel realm, ComponentModel component, boolean removed) { + notifyToRefreshPeriodicSync(session, realm, new UserStorageProviderModel(component), removed); + } + // Ensure all cluster nodes are notified - public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserStorageProviderModel provider, boolean removed) { + public static void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserStorageProviderModel provider, boolean removed) { UserStorageProviderFactory factory = (UserStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, provider.getProviderId()); if (!(factory instanceof ImportSynchronization) || !provider.isImportEnabled()) { return; @@ -180,7 +189,7 @@ public class UserStorageSyncManager { // Executed once it receives notification that some UserFederationProvider was created or updated - protected void refreshPeriodicSyncForProvider(final KeycloakSessionFactory sessionFactory, TimerProvider timer, final UserStorageProviderModel provider, final String realmId) { + protected static void refreshPeriodicSyncForProvider(final KeycloakSessionFactory sessionFactory, TimerProvider timer, final UserStorageProviderModel provider, final String realmId) { logger.debugf("Going to refresh periodic sync for provider '%s' . Full sync period: %d , changed users sync period: %d", provider.getName(), provider.getFullSyncPeriod(), provider.getChangedSyncPeriod()); @@ -198,7 +207,7 @@ public class UserStorageSyncManager { logger.debugf("Ignored periodic full sync with storage provider %s due small time since last sync", provider.getName()); } } catch (Throwable t) { - ServicesLogger.LOGGER.errorDuringFullUserSync(t); + logger.error("Error occurred during full sync of users", t); } } @@ -221,7 +230,7 @@ public class UserStorageSyncManager { logger.debugf("Ignored periodic changed-users sync with storage provider %s due small time since last sync", provider.getName()); } } catch (Throwable t) { - ServicesLogger.LOGGER.errorDuringChangedUserSync(t); + logger.error("Error occurred during sync of changed users", t); } } @@ -233,7 +242,7 @@ public class UserStorageSyncManager { } // Skip syncing if there is short time since last sync time. - private boolean shouldPerformNewPeriodicSync(int lastSyncTime, int period) { + private static boolean shouldPerformNewPeriodicSync(int lastSyncTime, int period) { if (lastSyncTime <= 0) { return true; } @@ -245,14 +254,14 @@ public class UserStorageSyncManager { } // Executed once it receives notification that some UserFederationProvider was removed - protected void removePeriodicSyncForProvider(TimerProvider timer, UserStorageProviderModel fedProvider) { + protected static void removePeriodicSyncForProvider(TimerProvider timer, UserStorageProviderModel fedProvider) { logger.debugf("Removing periodic sync for provider %s", fedProvider.getName()); timer.cancelTask(fedProvider.getId() + "-FULL"); timer.cancelTask(fedProvider.getId() + "-CHANGED"); } // Update interval of last sync for given UserFederationProviderModel. Do it in separate transaction - private void updateLastSyncInterval(final KeycloakSessionFactory sessionFactory, UserStorageProviderModel provider, final String realmId) { + private static void updateLastSyncInterval(final KeycloakSessionFactory sessionFactory, UserStorageProviderModel provider, final String realmId) { KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override @@ -274,7 +283,7 @@ public class UserStorageSyncManager { } - private class UserStorageClusterListener implements ClusterListener { + private static class UserStorageClusterListener implements ClusterListener { private final KeycloakSessionFactory sessionFactory; diff --git a/model/legacy-services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderRealmAdminProvider.java b/model/legacy-services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderRealmAdminProvider.java new file mode 100644 index 0000000000..d1249bc697 --- /dev/null +++ b/model/legacy-services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderRealmAdminProvider.java @@ -0,0 +1,40 @@ +package org.keycloak.services.resources.admin; + +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +public class UserStorageProviderRealmAdminProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider { + + @Override + public AdminRealmResourceProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "user-storage"; + } + + @Override + public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + return new UserStorageProviderResource(realm, auth, adminEvent); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java b/model/legacy-services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java similarity index 100% rename from services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java rename to model/legacy-services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java diff --git a/model/legacy-services/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory b/model/legacy-services/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory new file mode 100644 index 0000000000..030e4fd3ab --- /dev/null +++ b/model/legacy-services/src/main/resources/META-INF/services/org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory @@ -0,0 +1 @@ +org.keycloak.services.resources.admin.UserStorageProviderRealmAdminProvider diff --git a/model/legacy/src/main/java/org/keycloak/storage/LegacyStoreManagers.java b/model/legacy/src/main/java/org/keycloak/storage/LegacyStoreManagers.java new file mode 100644 index 0000000000..279394729a --- /dev/null +++ b/model/legacy/src/main/java/org/keycloak/storage/LegacyStoreManagers.java @@ -0,0 +1,20 @@ +package org.keycloak.storage; + +import org.keycloak.models.ClientProvider; +import org.keycloak.models.ClientScopeProvider; +import org.keycloak.models.GroupProvider; +import org.keycloak.models.RoleProvider; +import org.keycloak.models.UserProvider; + +public interface LegacyStoreManagers { + + ClientProvider clientStorageManager(); + + ClientScopeProvider clientScopeStorageManager(); + + RoleProvider roleStorageManager(); + + GroupProvider groupStorageManager(); + + UserProvider userStorageManager(); +} diff --git a/model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java b/model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java deleted file mode 100644 index da06a220bf..0000000000 --- a/model/legacy/src/main/java/org/keycloak/storage/datastore/LegacyDatastoreProviderFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.keycloak.storage.datastore; - -import org.keycloak.Config; -import org.keycloak.Config.Scope; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.storage.DatastoreProvider; -import org.keycloak.storage.DatastoreProviderFactory; - -public class LegacyDatastoreProviderFactory implements DatastoreProviderFactory { - - private static final String PROVIDER_ID = "legacy"; - private long clientStorageProviderTimeout; - private long roleStorageProviderTimeout; - - @Override - public DatastoreProvider create(KeycloakSession session) { - return new LegacyDatastoreProvider(this, session); - } - - @Override - public void init(Scope config) { - clientStorageProviderTimeout = Config.scope("client").getLong("storageProviderTimeout", 3000L); - roleStorageProviderTimeout = Config.scope("role").getLong("storageProviderTimeout", 3000L); - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - } - - @Override - public void close() { - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - public long getClientStorageProviderTimeout() { - return clientStorageProviderTimeout; - } - - public long getRoleStorageProviderTimeout() { - return roleStorageProviderTimeout; - } - -} diff --git a/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java b/model/legacy/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java rename to model/legacy/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java diff --git a/model/legacy/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/legacy/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..10bef50206 --- /dev/null +++ b/model/legacy/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -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.storage.UserStorageProviderSpi diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java index 0c603c44c6..655f8da08f 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java @@ -22,13 +22,13 @@ import org.keycloak.component.ComponentFactory; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserProvider; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.storage.OnCreateComponent; import org.keycloak.storage.OnUpdateComponent; -import org.keycloak.storage.UserStorageProviderFactory; import java.util.HashMap; import java.util.List; @@ -94,15 +94,17 @@ public class ComponentUtil { public static void notifyCreated(KeycloakSession session, RealmModel realm, ComponentModel model) { ComponentFactory factory = getComponentFactory(session, model); factory.onCreate(session, realm, model); - if (factory instanceof UserStorageProviderFactory) { - ((OnCreateComponent)session.userStorageManager()).onCreate(session, realm, model); + UserProvider users = session.users(); + if (users instanceof OnCreateComponent) { + ((OnCreateComponent) users).onCreate(session, realm, model); } } public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) { ComponentFactory factory = getComponentFactory(session, newModel); factory.onUpdate(session, realm, oldModel, newModel); - if (factory instanceof UserStorageProviderFactory) { - ((OnUpdateComponent)session.userStorageManager()).onUpdate(session, realm, oldModel, newModel); + UserProvider users = session.users(); + if (users instanceof OnUpdateComponent) { + ((OnUpdateComponent) users).onUpdate(session, realm, oldModel, newModel); } } public static void notifyPreRemove(KeycloakSession session, RealmModel realm, ComponentModel model) { diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java index 73889e05fd..bd499e6b93 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java @@ -17,6 +17,7 @@ package org.keycloak.models.utils; +import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderEvent; /** @@ -25,4 +26,14 @@ import org.keycloak.provider.ProviderEvent; * @author Marek Posolda */ public class PostMigrationEvent implements ProviderEvent { + + private final KeycloakSessionFactory factory; + + public PostMigrationEvent(KeycloakSessionFactory factory) { + this.factory = factory; + } + + public KeycloakSessionFactory getFactory() { + return this.factory; + } } diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClusterAwareScheduledTaskRunner.java b/server-spi-private/src/main/java/org/keycloak/services/scheduled/ClusterAwareScheduledTaskRunner.java similarity index 100% rename from services/src/main/java/org/keycloak/services/scheduled/ClusterAwareScheduledTaskRunner.java rename to server-spi-private/src/main/java/org/keycloak/services/scheduled/ClusterAwareScheduledTaskRunner.java diff --git a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java b/server-spi-private/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java similarity index 92% rename from services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java rename to server-spi-private/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java index 97dccc030c..350a6c72e7 100644 --- a/services/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java +++ b/server-spi-private/src/main/java/org/keycloak/services/scheduled/ScheduledTaskRunner.java @@ -21,7 +21,6 @@ import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.services.ServicesLogger; import org.keycloak.timer.ScheduledTask; /** @@ -54,7 +53,7 @@ public class ScheduledTaskRunner implements Runnable { } runTask(session); } catch (Throwable t) { - ServicesLogger.LOGGER.failedToRunScheduledTask(t, task.getClass().getSimpleName()); + logger.errorf(t, "Failed to run scheduled task %s", task.getClass().getSimpleName()); session.getTransactionManager().rollback(); } finally { @@ -64,7 +63,7 @@ public class ScheduledTaskRunner implements Runnable { try { session.close(); } catch (Throwable t) { - ServicesLogger.LOGGER.failedToCloseProviderSession(t); + logger.errorf(t, "Failed to close ProviderSession"); } } } diff --git a/server-spi-private/src/main/java/org/keycloak/storage/DatastoreProvider.java b/server-spi-private/src/main/java/org/keycloak/storage/DatastoreProvider.java index edd21e4913..a7077e97a8 100644 --- a/server-spi-private/src/main/java/org/keycloak/storage/DatastoreProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/storage/DatastoreProvider.java @@ -5,6 +5,7 @@ import org.keycloak.models.ClientScopeProvider; import org.keycloak.models.GroupProvider; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleProvider; +import org.keycloak.models.UserProvider; import org.keycloak.provider.Provider; public interface DatastoreProvider extends Provider { @@ -19,4 +20,6 @@ public interface DatastoreProvider extends Provider { public RoleProvider roles(); + public UserProvider users(); + } diff --git a/server-spi-private/src/main/java/org/keycloak/storage/LegacyStoreSyncEvent.java b/server-spi-private/src/main/java/org/keycloak/storage/LegacyStoreSyncEvent.java new file mode 100644 index 0000000000..54ba48ffb4 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/LegacyStoreSyncEvent.java @@ -0,0 +1,55 @@ +/* + * 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.storage; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderEvent; + +/** + * Event for notifying legacy store about the need to reconfigure user providers + * sychronization. + */ +public class LegacyStoreSyncEvent implements ProviderEvent { + + private final KeycloakSession session; + private final RealmModel realm; + private final boolean removed; + + public LegacyStoreSyncEvent(KeycloakSession session, RealmModel realm, boolean removed) { + this.session = session; + this.realm = realm; + this.removed = removed; + } + + public static void fire(KeycloakSession session, RealmModel realm, boolean removed) { + session.getKeycloakSessionFactory().publish(new LegacyStoreSyncEvent(session, realm, removed)); + } + + public KeycloakSession getSession() { + return session; + } + + public RealmModel getRealm() { + return realm; + } + + public boolean getRemoved() { + return removed; + } + +} \ No newline at end of file diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8174bc578d..e3badf2987 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -17,7 +17,6 @@ org.keycloak.component.ComponentFactorySpi org.keycloak.provider.ExceptionConverterSpi -org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi org.keycloak.models.ClientSpi org.keycloak.models.ClientScopeSpi diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java index d312c6e078..bfc1b2b29e 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java @@ -90,9 +90,9 @@ public class PermissionTicketService { UserModel user = null; if(representation.getRequester() != null) - user = this.authorization.getKeycloakSession().userStorageManager().getUserById(this.authorization.getRealm(), representation.getRequester()); + user = this.authorization.getKeycloakSession().users().getUserById(this.authorization.getRealm(), representation.getRequester()); else - user = this.authorization.getKeycloakSession().userStorageManager().getUserByUsername(this.authorization.getRealm(), representation.getRequesterName()); + user = this.authorization.getKeycloakSession().users().getUserByUsername(this.authorization.getRealm(), representation.getRequesterName()); if (user == null) throw new ErrorResponseException("invalid_permission", "Requester does not exists in this server as user.", Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index af467879c7..4537d0b213 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -47,7 +47,6 @@ import org.keycloak.provider.InvalidationHandler.ObjectType; import org.keycloak.services.clientpolicy.ClientPolicyManager; import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.storage.DatastoreProvider; -import org.keycloak.storage.UserStorageManager; import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.vault.DefaultVaultTranscriber; import org.keycloak.vault.VaultProvider; @@ -77,7 +76,6 @@ public class DefaultKeycloakSession implements KeycloakSession { private final Map attributes = new HashMap<>(); private final Map> invalidationMap = new HashMap<>(); private DatastoreProvider datastoreProvider; - private UserStorageManager userStorageManager; private UserCredentialStoreManager userCredentialStorageManager; private UserSessionProvider sessionProvider; private UserLoginFailureProvider userLoginFailureProvider; @@ -217,21 +215,14 @@ public class DefaultKeycloakSession implements KeycloakSession { return groups(); } - @Override public UserProvider userStorageManager() { - if (userStorageManager == null) userStorageManager = new UserStorageManager(this); - return userStorageManager; + return users(); } @Override public UserProvider users() { - UserCache cache = getProvider(UserCache.class); - if (cache != null) { - return cache; - } else { - return userStorageManager(); - } + return getDatastoreProvider().users(); } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 472b701023..306eb04ca8 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -52,6 +52,7 @@ import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.sessions.AuthenticationSessionProvider; +import org.keycloak.storage.LegacyStoreSyncEvent; import org.keycloak.services.clientregistration.policy.DefaultClientRegistrationPolicies; import java.util.Collections; @@ -271,10 +272,7 @@ public class RealmManager { } // Refresh periodic sync tasks for configured storageProviders - UserStorageSyncManager storageSync = new UserStorageSyncManager(); - realm.getUserStorageProvidersStream() - .forEachOrdered(provider -> storageSync.notifyToRefreshPeriodicSync(session, realm, provider, true)); - + LegacyStoreSyncEvent.fire(session, realm, true); } return removed; } @@ -588,9 +586,7 @@ public class RealmManager { } // Refresh periodic sync tasks for configured storageProviders - UserStorageSyncManager storageSync = new UserStorageSyncManager(); - realm.getUserStorageProvidersStream() - .forEachOrdered(provider -> storageSync.notifyToRefreshPeriodicSync(session, realm, provider, false)); + LegacyStoreSyncEvent.fire(session, realm, false); setupAuthorizationServices(realm); setupClientRegistrations(realm); diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 81cff07d05..7cb6993819 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -45,15 +45,8 @@ import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler; import org.keycloak.services.filters.KeycloakSecurityHeadersFilter; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.managers.UserStorageSyncManager; import org.keycloak.services.resources.admin.AdminRoot; -import org.keycloak.services.scheduled.ClearExpiredClientInitialAccessTokens; -import org.keycloak.services.scheduled.ClearExpiredEvents; -import org.keycloak.services.scheduled.ClearExpiredUserSessions; -import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner; -import org.keycloak.services.scheduled.ScheduledTaskRunner; import org.keycloak.services.util.ObjectMapperResolver; -import org.keycloak.timer.TimerProvider; import org.keycloak.transaction.JtaTransactionManagerLookup; import org.keycloak.util.JsonSerialization; @@ -156,9 +149,7 @@ public class KeycloakApplication extends Application { }); - sessionFactory.publish(new PostMigrationEvent()); - - setupScheduledTasks(sessionFactory); + sessionFactory.publish(new PostMigrationEvent(sessionFactory)); } protected void shutdown() { @@ -236,21 +227,6 @@ public class KeycloakApplication extends Application { return factory; } - public static void setupScheduledTasks(final KeycloakSessionFactory sessionFactory) { - long interval = Config.scope("scheduled").getLong("interval", 900L) * 1000; - - KeycloakSession session = sessionFactory.create(); - try { - TimerProvider timer = session.getProvider(TimerProvider.class); - timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents"); - timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens"); - timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, ClearExpiredUserSessions.TASK_NAME); - new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer); - } finally { - session.close(); - } - } - public static KeycloakSessionFactory getSessionFactory() { return sessionFactory; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 5559092f86..b65284a614 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -112,10 +112,11 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.LDAPServerCapabilitiesManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; -import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.storage.LegacyStoreSyncEvent; import org.keycloak.utils.ProfileHelper; import org.keycloak.utils.ReservedCharValidator; @@ -436,9 +437,7 @@ public class RealmAdminResource { RepresentationToModel.updateRealm(rep, realm, session); // Refresh periodic sync tasks for configured federationProviders - UserStorageSyncManager usersSyncManager = new UserStorageSyncManager(); - realm.getUserStorageProvidersStream().forEachOrdered(fedProvider -> - usersSyncManager.notifyToRefreshPeriodicSync(session, realm, fedProvider, false)); + LegacyStoreSyncEvent.fire(session, realm, false); // This populates the map in DefaultKeycloakContext to be used when treating the event session.getContext().getUri(); @@ -531,12 +530,18 @@ public class RealmAdminResource { } - @Path("user-storage") - public UserStorageProviderResource userStorage() { - UserStorageProviderResource fed = new UserStorageProviderResource(realm, auth, adminEvent); - ResteasyProviderFactory.getInstance().injectProperties(fed); - //resourceContext.initResource(fed); - return fed; + @Path("{extension}") + public Object extension(@PathParam("extension") String extension) { + AdminRealmResourceProvider provider = session.getProvider(AdminRealmResourceProvider.class, extension); + if (provider != null) { + Object resource = provider.getResource(session, realm, auth, adminEvent); + if (resource != null) { + ResteasyProviderFactory.getInstance().injectProperties(resource); + return resource; + } + } + + throw new NotFoundException(); } @Path("authentication") diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProvider.java b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProvider.java new file mode 100644 index 0000000000..8ca009acec --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProvider.java @@ -0,0 +1,41 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.resources.admin.ext; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; + +/** + *

A {@link AdminRealmResourceProvider} creates JAX-RS sub-resource instances for paths relative + * to Realm's RESTful Admin API that could not be resolved by the server. + */ +public interface AdminRealmResourceProvider extends Provider { + + /** + *

Returns a JAX-RS resource instance. + * + * @return a JAX-RS sub-resource instance + */ + Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, + AdminEventBuilder adminEvent); + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProviderFactory.java b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProviderFactory.java new file mode 100644 index 0000000000..7e89d92d3d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceProviderFactory.java @@ -0,0 +1,28 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.resources.admin.ext; + +import org.keycloak.provider.ProviderFactory; + +/** + *

A factory that creates {@link AdminRealmResourceProvider} instances. + */ +public interface AdminRealmResourceProviderFactory extends ProviderFactory { + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceSpi.java b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceSpi.java new file mode 100644 index 0000000000..93c2ca21a3 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ext/AdminRealmResourceSpi.java @@ -0,0 +1,52 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual 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.resources.admin.ext; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + *

A {@link Spi} to plug additional sub-resources to Realms' RESTful Admin API. + * + *

Implementors can use this {@link Spi} to provide additional services to the mentioned API and extend Keycloak capabilities by + * creating JAX-RS sub-resources for paths not known by the server. + */ +public class AdminRealmResourceSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "admin-realm-restapi-extension"; + } + + @Override + public Class getProviderClass() { + return AdminRealmResourceProvider.class; + } + + @Override + public Class> getProviderFactoryClass() { + return AdminRealmResourceProviderFactory.class; + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 703057b3e9..f1dbb02f44 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -26,4 +26,5 @@ org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessorSpi org.keycloak.encoding.ResourceEncodingSpi org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelSpi org.keycloak.protocol.oidc.grants.ciba.resolvers.CIBALoginUserResolverSpi -org.keycloak.protocol.oidc.rar.AuthorizationRequestParserSpi \ No newline at end of file +org.keycloak.protocol.oidc.rar.AuthorizationRequestParserSpi +org.keycloak.services.resources.admin.ext.AdminRealmResourceSpi diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java index bd884688b3..edfdc67db5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java @@ -32,7 +32,8 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.storage.managers.UserStorageSyncManager; +import org.keycloak.storage.UserStoragePrivateUtil; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.idm.model.LDAPObject; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSyncTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSyncTest.java index b46f75fe72..882cf8e8ed 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSyncTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPSyncTest.java @@ -40,7 +40,9 @@ import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.SynchronizationResultRepresentation; -import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.storage.managers.UserStorageSyncManager; +import org.keycloak.storage.UserStoragePrivateUtil; +import org.keycloak.storage.UserStorageUtil; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProviderFactory; import org.keycloak.storage.ldap.LDAPUtils; diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java index d049319140..24c674d468 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java @@ -51,6 +51,8 @@ import org.keycloak.provider.ProviderManager; import org.keycloak.provider.Spi; import org.keycloak.services.DefaultComponentFactoryProviderFactory; import org.keycloak.services.DefaultKeycloakSessionFactory; +import org.keycloak.storage.DatastoreProviderFactory; +import org.keycloak.storage.DatastoreSpi; import org.keycloak.timer.TimerSpi; import com.google.common.collect.ImmutableSet; @@ -227,6 +229,7 @@ public abstract class KeycloakModelTest { .add(UserLoginFailureSpi.class) .add(UserSessionSpi.class) .add(UserSpi.class) + .add(DatastoreSpi.class) .build(); private static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder() @@ -234,6 +237,7 @@ public abstract class KeycloakModelTest { .add(DefaultAuthorizationProviderFactory.class) .add(DefaultExecutorsProviderFactory.class) .add(DeploymentStateProviderFactory.class) + .add(DatastoreProviderFactory.class) .build(); protected static final List MODEL_PARAMETERS; @@ -312,7 +316,7 @@ public abstract class KeycloakModelTest { } }; res.init(); - res.publish(new PostMigrationEvent()); + res.publish(new PostMigrationEvent(res)); return res; } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSyncTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSyncTest.java index 336791dc55..b6b5d2b0c3 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSyncTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserSyncTest.java @@ -9,7 +9,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.UserProvider; -import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.storage.managers.UserStorageSyncManager; +import org.keycloak.storage.UserStoragePrivateUtil; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel;