KEYCLOAK-17409 Support for amphibian (both component and standalone) provider

This commit is contained in:
Hynek Mlnarik 2021-02-21 09:45:23 +01:00 committed by Hynek Mlnařík
parent 5fac80b05e
commit a36fafe04e
29 changed files with 1318 additions and 8 deletions

View file

@ -41,12 +41,23 @@ import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jgroups.JChannel; import org.jgroups.JChannel;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.cluster.ManagedCacheManagerProvider;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.InvalidationHandler.ObjectType;
import org.keycloak.provider.ProviderEvent;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS;
import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -95,7 +106,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {
factory.register((ProviderEvent event) -> {
if (event instanceof PostMigrationEvent) {
KeycloakModelUtils.runJobInTransaction(factory, session -> { registerSystemWideListeners(session); });
}
});
} }
protected void lazyInit() { protected void lazyInit() {
@ -495,4 +510,23 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} }
} }
private void registerSystemWideListeners(KeycloakSession session) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
if (event instanceof ClearCacheEvent) {
sessionFactory.invalidate(ObjectType._ALL_);
}
});
cluster.registerListener(REALM_INVALIDATION_EVENTS, (ClusterEvent event) -> {
if (event instanceof RealmUpdatedEvent) {
RealmUpdatedEvent rr = (RealmUpdatedEvent) event;
sessionFactory.invalidate(ObjectType.REALM, rr.getId());
} else if (event instanceof RealmRemovedEvent) {
RealmRemovedEvent rr = (RealmRemovedEvent) event;
sessionFactory.invalidate(ObjectType.REALM, rr.getId());
}
});
}
} }

View file

@ -71,11 +71,18 @@ public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionF
} }
} }
// Component factory must be initialized first, so that postInit in other factories can use component factories
updateComponentFactoryProviderFactory();
if (componentFactoryPF != null) {
componentFactoryPF.postInit(this);
}
for (Map<String, ProviderFactory> f : factoriesMap.values()) { for (Map<String, ProviderFactory> f : factoriesMap.values()) {
for (ProviderFactory factory : f.values()) { for (ProviderFactory factory : f.values()) {
if (factory != componentFactoryPF) {
factory.postInit(this); factory.postInit(this);
} }
} }
}
AdminPermissions.registerListener(this); AdminPermissions.registerListener(this);
// make the session factory ready for hot deployment // make the session factory ready for hot deployment

View file

@ -0,0 +1,90 @@
/*
* Copyright 2021 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.component;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.InvalidationHandler.ObjectType;
import java.util.Collections;
import java.util.List;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import java.util.Objects;
/**
* Ancestor for a provider factory for both a standalone {@link ProviderFactory} and a {@link ComponentFactory}. It
* behaves as usual for a standalone provider, and for a component creates a factory customized according to
* configuration of this component. The component creation then behaves in the same way as if it was
* a standalone component, i.e.:
* <ul>
* <li>The component-specific factory is initialized via {@link #init} method where the configuration
* is taken from the component configuration, converted into a {@link Scope}. The
* component configuration takes precedence over configuration of the provider factory.</li>
* <li>Creation of the instances is done via standard {@link #create(KeycloakSession)} method even for components,
* since there is now a specific factory per component.</li>
* <li>Component-specific factories are cached inside the provider factory
* similarly to how provider factories are cached in the session factory.</li>
* </ul>
*
* @see ComponentFactoryProviderFactory
*
* @author hmlnarik
*/
public interface AmphibianProviderFactory<ProviderType extends Provider> extends ProviderFactory<ProviderType>, ComponentFactory<ProviderType, ProviderType> {
@Override
ProviderType create(KeycloakSession session);
@Override
@Deprecated
default ProviderType create(KeycloakSession session, ComponentModel model) {
throw new UnsupportedOperationException("Use create(KeycloakSession) instead");
}
@Override
default List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
default void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
String oldId = oldModel == null ? null : oldModel.getId();
String newId = newModel == null ? null : newModel.getId();
if (oldId != null) {
if (newId == null || Objects.equals(oldId, newId)) {
session.invalidate(ObjectType.COMPONENT, oldId);
} else {
session.invalidate(ObjectType.COMPONENT, oldId, newId);
}
} else if (newId != null) {
session.invalidate(ObjectType.COMPONENT, newId);
}
}
@Override
default void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) {
if (model != null && model.getId() != null) {
session.invalidate(ObjectType.COMPONENT, model.getId());
}
}
@Override
default void close() {
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 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.component;
import org.keycloak.provider.Provider;
/**
*
* @author hmlnarik
*/
public interface ComponentFactoryProvider extends Provider {
@Override
default void close() {};
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 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.component;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import java.util.function.Function;
import org.keycloak.component.ComponentModel;
/**
*
* @author hmlnarik
*/
public interface ComponentFactoryProviderFactory extends ProviderFactory<ComponentFactoryProvider>, InvalidationHandler {
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String realmId, String componentId, Function<KeycloakSessionFactory, ComponentModel> model);
@Override
default ComponentFactoryProvider create(KeycloakSession session) {
throw new UnsupportedOperationException("ComponentFactoryProvider is session-independent, hence not instantiable per session.");
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2021 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.component;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
*
* @author hmlnarik
*/
public class ComponentFactorySpi implements Spi {
public static final String NAME = "componentFactory";
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return ComponentFactoryProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ComponentFactoryProviderFactory.class;
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2021 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.component;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
/**
*
* @author hmlnarik
*/
public class ComponentModelScope implements Scope {
private final Scope origScope;
private final ComponentModel componentConfig;
private final String prefix;
public ComponentModelScope(Scope origScope, ComponentModel componentConfig) {
this(origScope, componentConfig, "");
}
public ComponentModelScope(Scope origScope, ComponentModel componentConfig, String prefix) {
this.origScope = origScope;
this.componentConfig = componentConfig;
this.prefix = prefix;
}
public String getComponentId() {
return componentConfig.getId();
}
public String getComponentName() {
return componentConfig.getName();
}
public <T> T getComponentNote(String key) {
return componentConfig.getNote(key);
}
public String getComponentParentId() {
return componentConfig.getParentId();
}
public String getComponentSubType() {
return componentConfig.getSubType();
}
@Override
public String get(String key) {
return get(key, null);
}
@Override
public String get(String key, String defaultValue) {
final String res = componentConfig.get(prefix + key, null);
return (res == null) ? origScope.get(key, defaultValue) : res;
}
@Override
public String[] getArray(String key) {
final String[] res = get(prefix + key, "").split("\\s*,\\s*");
return (res == null) ? origScope.getArray(key) : res;
}
@Override
public Integer getInt(String key) {
return getInt(key, null);
}
@Override
public Integer getInt(String key, Integer defaultValue) {
final String res = componentConfig.get(prefix + key, null);
return (res == null) ? origScope.getInt(key, defaultValue) : Integer.parseInt(res);
}
@Override
public Long getLong(String key) {
return getLong(key, null);
}
@Override
public Long getLong(String key, Long defaultValue) {
final String res = componentConfig.get(prefix + key, null);
return (res == null) ? origScope.getLong(key, defaultValue) : Long.parseLong(res);
}
@Override
public Boolean getBoolean(String key) {
return getBoolean(key, null);
}
@Override
public Boolean getBoolean(String key, Boolean defaultValue) {
final String res = componentConfig.get(prefix + key, null);
return (res == null) ? origScope.getBoolean(key, defaultValue) : Boolean.parseBoolean(res);
}
@Override
public Scope scope(String... scope) {
return new ComponentModelScope(origScope.scope(scope), componentConfig, String.join(".", scope) + ".");
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.keycloak.Config;
import org.keycloak.Config.Scope;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.CertificateUtils;
@ -64,6 +66,10 @@ import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
/** /**
* Set of helper methods, which are useful in various model implementations. * Set of helper methods, which are useful in various model implementations.
@ -299,6 +305,93 @@ public final class KeycloakModelUtils {
} }
public static Function<KeycloakSessionFactory, ComponentModel> componentModelGetter(String realmId, String componentId) {
return factory -> getComponentModel(factory, realmId, componentId);
}
public static ComponentModel getComponentModel(KeycloakSessionFactory factory, String realmId, String componentId) {
AtomicReference<ComponentModel> cm = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(factory, session -> {
RealmModel realm = session.realms().getRealm(realmId);
cm.set(realm == null ? null : realm.getComponent(componentId));
});
return cm.get();
}
public static <T extends Provider> ProviderFactory<T> getComponentFactory(KeycloakSessionFactory factory, Class<T> providerClass, Scope config, String spiName) {
String realmId = config.get("realmId");
String componentId = config.get("componentId");
if (realmId == null || componentId == null) {
realmId = "ROOT";
ComponentModel cm = new ScopeComponentModel(providerClass, config, spiName);
return factory.getProviderFactory(providerClass, realmId, cm.getId(), k -> cm);
} else {
return factory.getProviderFactory(providerClass, realmId, componentId, componentModelGetter(realmId, componentId));
}
}
private static class ScopeComponentModel extends ComponentModel {
private final String componentId;
private final String providerId;
private final String providerType;
private final Scope config;
public ScopeComponentModel(Class<?> providerClass, Scope baseConfiguration, String spiName) {
final String pr = baseConfiguration.get("provider", Config.getProvider(spiName));
this.providerId = pr == null ? "default" : pr;
this.config = baseConfiguration.scope(this.providerId);
this.componentId = spiName + "-" + this.providerId;
this.providerType = providerClass.getName();
}
@Override
public String getProviderType() {
return providerType;
}
@Override
public String getProviderId() {
return providerId;
}
@Override
public String getName() {
return componentId + "-config";
}
@Override
public String getId() {
return componentId;
}
@Override
public boolean get(String key, boolean defaultValue) {
return config.getBoolean(key, defaultValue);
}
@Override
public long get(String key, long defaultValue) {
return config.getLong(key, defaultValue);
}
@Override
public int get(String key, int defaultValue) {
return config.getInt(key, defaultValue);
}
@Override
public String get(String key, String defaultValue) {
return config.get(key, defaultValue);
}
@Override
public String get(String key) {
return get(key, null);
}
}
public static String getMasterRealmAdminApplicationClientId(String realmName) { public static String getMasterRealmAdminApplicationClientId(String realmName) {
return realmName + "-realm"; return realmName + "-realm";

View file

@ -15,6 +15,7 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.component.ComponentFactorySpi
org.keycloak.provider.ExceptionConverterSpi org.keycloak.provider.ExceptionConverterSpi
org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi

View file

@ -19,6 +19,8 @@ package org.keycloak.models;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.UserCache;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.services.clientpolicy.ClientPolicyManager; import org.keycloak.services.clientpolicy.ClientPolicyManager;
import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.sessions.AuthenticationSessionProvider;
@ -31,7 +33,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface KeycloakSession { public interface KeycloakSession extends InvalidationHandler {
KeycloakContext getContext(); KeycloakContext getContext();
@ -62,6 +64,25 @@ public interface KeycloakSession {
*/ */
<T extends Provider> T getProvider(Class<T> clazz, String id); <T extends Provider> T getProvider(Class<T> clazz, String id);
/**
* Returns a component provider for a component from the realm that is relevant to this session.
* The relevant realm must be set prior to calling this method in the context, see {@link KeycloakContext#getRealm()}.
* @param <T>
* @param clazz
* @param componentId Component configuration
* @throws IllegalArgumentException If the realm is not set in the context.
* @return Provider configured according to the {@link componentId}, {@code null} if it cannot be instantiated.
*/
<T extends Provider> T getComponentProvider(Class<T> clazz, String componentId);
/**
*
* @param <T>
* @param clazz
* @param componentModel
* @return
* @deprecated Deprecated in favor of {@link #getComponentProvider)
*/
<T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel); <T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel);
/** /**
@ -92,6 +113,13 @@ public interface KeycloakSession {
Object removeAttribute(String attribute); Object removeAttribute(String attribute);
void setAttribute(String name, Object value); void setAttribute(String name, Object value);
/**
* Invalidates intermediate states of the given objects, both immediately and at the end of this session.
* @param type Type of the objects to invalidate
* @param ids Identifiers of the invalidated objects
*/
@Override
void invalidate(InvalidableObjectType type, Object... ids);
void enlistForClose(Provider provider); void enlistForClose(Provider provider);

View file

@ -17,6 +17,8 @@
package org.keycloak.models; package org.keycloak.models;
import org.keycloak.component.ComponentModel;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEventManager; import org.keycloak.provider.ProviderEventManager;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
@ -24,6 +26,7 @@ import org.keycloak.provider.Spi;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -31,7 +34,8 @@ import java.util.stream.Stream;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface KeycloakSessionFactory extends ProviderEventManager { public interface KeycloakSessionFactory extends ProviderEventManager, InvalidationHandler {
KeycloakSession create(); KeycloakSession create();
Set<Spi> getSpis(); Set<Spi> getSpis();
@ -42,6 +46,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager {
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id); <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String realmId, String componentId, Function<KeycloakSessionFactory, ComponentModel> modelGetter);
/** /**
* Returns list of provider factories for the given provider. * Returns list of provider factories for the given provider.
* @param clazz {@code Class<? extends Provider>} * @param clazz {@code Class<? extends Provider>}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2021 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.provider;
/**
* Handles invalidation requests. This interface is specifically implemented by
* providers that implement a cache of objects that might change in the outside.
* <p>
* Note that implementors are expected to react to invalidation requests:
* invalidate the objects in the cache. They should <b>not</b> initiate
* invalidation of the same objects neither locally nor via network - that
* could result in an infinite loop.
*
* @author hmlnarik
*/
public interface InvalidationHandler {
/**
* Tagging interface for the kinds of invalidatable object
*/
public interface InvalidableObjectType {}
public enum ObjectType implements InvalidableObjectType {
_ALL_, REALM, CLIENT, CLIENT_SCOPE, USER, ROLE, GROUP, COMPONENT, PROVIDER_FACTORY
}
/**
* Invalidates intermediate states of the given objects
* @param type Type of the objects to invalidate
* @param ids Identifiers of the invalidated objects
*/
void invalidate(InvalidableObjectType type, Object... ids);
}

View file

@ -0,0 +1,187 @@
/*
* Copyright 2021 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;
import org.keycloak.Config;
import org.keycloak.Config.Scope;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.StackUtil;
import org.keycloak.component.ComponentFactoryProviderFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentModelScope;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
import org.keycloak.provider.InvalidationHandler.ObjectType;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
/**
* @author hmlnarik
*/
public class DefaultComponentFactoryProviderFactory implements ComponentFactoryProviderFactory {
private static final Logger LOG = Logger.getLogger(DefaultComponentFactoryProviderFactory.class);
public static final String PROVIDER_ID = "default";
private final AtomicReference<ConcurrentMap<String, ProviderFactory>> componentsMap = new AtomicReference<>(new ConcurrentHashMap<>());
/**
* Should an ID in the key be invalidated, it would invalidate also all the IDs in the values
*/
private final ConcurrentMap<Object, Set<String>> dependentInvalidations = new ConcurrentHashMap<>();
private KeycloakSessionFactory factory;
private boolean componentCachingAvailable;
private boolean componentCachingEnabled;
@Override
public void init(Scope config) {
this.componentCachingEnabled = config.getBoolean("cachingEnabled", true);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
this.factory = factory;
this.componentCachingAvailable = this.componentCachingEnabled && this.factory.getProviderFactory(ClusterProvider.class) != null;
if (! componentCachingEnabled) {
LOG.warn("Caching of components disabled by the configuration which may have performance impact.");
} else if (! componentCachingAvailable) {
LOG.warn("No system-wide ClusterProviderFactory found. Cannot send messages across cluster, thus disabling caching of components.");
}
}
@Override
@SuppressWarnings("unchecked")
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String realmId, String componentId, Function<KeycloakSessionFactory, ComponentModel> modelGetter) {
ProviderFactory res = componentsMap.get().get(componentId);
if (res != null) {
LOG.tracef("Found cached ProviderFactory for %s in (%s, %s)", clazz, realmId, componentId);
return res;
}
// Apply the expensive operation before putting it into the cache
final ComponentModel cm;
if (modelGetter == null) {
LOG.debugf("Getting component configuration for component (%s, %s) from realm configuration", clazz, realmId, componentId);
cm = KeycloakModelUtils.getComponentModel(factory, realmId, componentId);
} else {
LOG.debugf("Getting component configuration for component (%s, %s) via provided method", realmId, componentId);
cm = modelGetter.apply(factory);
}
if (cm == null) {
return null;
}
final String provider = cm.getProviderId();
ProviderFactory<T> pf = provider == null
? factory.getProviderFactory(clazz)
: factory.getProviderFactory(clazz, provider);
if (pf == null) { // Either not found or not enabled
LOG.debugf("ProviderFactory for %s in (%s, %s) not found", clazz, realmId, componentId);
return null;
}
final ProviderFactory newFactory;
try {
newFactory = pf.getClass().getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException ex) {
LOG.warn("Cannot instantiate factory", ex);
return null;
}
Scope scope = Config.scope(factory.getSpi(clazz).getName(), provider);
ComponentModelScope configScope = new ComponentModelScope(scope, cm);
return this.componentCachingAvailable
? componentsMap.get().computeIfAbsent(componentId, cId -> initializeFactory(clazz, realmId, componentId, newFactory, configScope))
: initializeFactory(clazz, realmId, componentId, newFactory, configScope);
}
@SuppressWarnings("unchecked")
protected <T extends Provider> ProviderFactory<T> initializeFactory(Class<T> clazz, String realmId, String componentId, final ProviderFactory newFactory, ComponentModelScope configScope) {
LOG.debugf("Initializing ProviderFactory for %s in (%s, %s)", clazz, realmId, componentId);
newFactory.init(configScope);
newFactory.postInit(factory);
dependentInvalidations.computeIfAbsent(realmId, k -> ConcurrentHashMap.newKeySet()).add(componentId);
dependentInvalidations.computeIfAbsent(newFactory.getClass(), k -> ConcurrentHashMap.newKeySet()).add(componentId);
return newFactory;
}
@Override
public void invalidate(InvalidableObjectType type, Object... ids) {
if (LOG.isDebugEnabled()) {
LOG.debugf("Invalidating %s: %s", type, Arrays.asList(ids));
}
LOG.tracef("invalidate(%s)%s", type, StackUtil.getShortStackTrace());
if (type == ObjectType._ALL_) {
final ConcurrentMap<String, ProviderFactory> cm = componentsMap.getAndSet(new ConcurrentHashMap<>());
dependentInvalidations.clear();
cm.values().forEach(ProviderFactory::close);
} else if (type == ObjectType.COMPONENT) {
Stream.of(ids)
.map(componentsMap.get()::remove).filter(Objects::nonNull)
.forEach(ProviderFactory::close);
propagateInvalidation(componentsMap.get(), type, ids);
} else if (type == ObjectType.REALM || type == ObjectType.PROVIDER_FACTORY) {
Stream.of(ids)
.map(dependentInvalidations::get).filter(Objects::nonNull).flatMap(Collection::stream)
.map(componentsMap.get()::remove).filter(Objects::nonNull)
.forEach(ProviderFactory::close);
Stream.of(ids).forEach(dependentInvalidations::remove);
propagateInvalidation(componentsMap.get(), type, ids);
} else {
propagateInvalidation(componentsMap.get(), type, ids);
}
}
private void propagateInvalidation(ConcurrentMap<String, ProviderFactory> componentsMap, InvalidableObjectType type, Object[] ids) {
componentsMap.values()
.stream()
.filter(InvalidationHandler.class::isInstance)
.map(InvalidationHandler.class::cast)
.forEach(ih -> ih.invalidate(type, ids));
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void close() {
componentsMap.get().values().forEach(ProviderFactory::close);
}
}

View file

@ -30,6 +30,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransactionManager; import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.models.KeyManager; import org.keycloak.models.KeyManager;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider; import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleProvider; import org.keycloak.models.RoleProvider;
import org.keycloak.models.ThemeManager; import org.keycloak.models.ThemeManager;
@ -38,6 +39,8 @@ import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.clientpolicy.ClientPolicyManager; import org.keycloak.services.clientpolicy.ClientPolicyManager;
@ -53,10 +56,13 @@ import org.keycloak.vault.DefaultVaultTranscriber;
import org.keycloak.vault.VaultProvider; import org.keycloak.vault.VaultProvider;
import org.keycloak.vault.VaultTranscriber; import org.keycloak.vault.VaultTranscriber;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -71,6 +77,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
private final List<Provider> closable = new LinkedList<>(); private final List<Provider> closable = new LinkedList<>();
private final DefaultKeycloakTransactionManager transactionManager; private final DefaultKeycloakTransactionManager transactionManager;
private final Map<String, Object> attributes = new HashMap<>(); private final Map<String, Object> attributes = new HashMap<>();
private final Map<InvalidableObjectType, Set<Object>> invalidationMap = new HashMap<>();
private RealmProvider model; private RealmProvider model;
private ClientProvider clientProvider; private ClientProvider clientProvider;
private ClientScopeProvider clientScopeProvider; private ClientScopeProvider clientScopeProvider;
@ -158,6 +165,12 @@ public class DefaultKeycloakSession implements KeycloakSession {
} }
@Override
public void invalidate(InvalidableObjectType type, Object... ids) {
factory.invalidate(type, ids);
invalidationMap.computeIfAbsent(type, o -> new HashSet<>()).addAll(Arrays.asList(ids));
}
@Override @Override
public void enlistForClose(Provider provider) { public void enlistForClose(Provider provider) {
closable.add(provider); closable.add(provider);
@ -322,6 +335,30 @@ public class DefaultKeycloakSession implements KeycloakSession {
return provider; return provider;
} }
@Override
@SuppressWarnings("unchecked")
public <T extends Provider> T getComponentProvider(Class<T> clazz, String componentId) {
Integer hash = clazz.hashCode() + componentId.hashCode();
T provider = (T) providers.get(hash);
final RealmModel realm = getContext().getRealm();
if (realm == null) {
throw new IllegalArgumentException("Realm not set in the context.");
}
// KEYCLOAK-11890 - Avoid using HashMap.computeIfAbsent() to implement logic in outer if() block below,
// since per JDK-8071667 the remapping function should not modify the map during computation. While
// allowed on JDK 1.8, attempt of such a modification throws ConcurrentModificationException with JDK 9+
if (provider == null) {
final String realmId = realm.getId();
ProviderFactory<T> providerFactory = factory.getProviderFactory(clazz, realmId, componentId, KeycloakModelUtils.componentModelGetter(realmId, componentId));
if (providerFactory != null) {
provider = providerFactory.create(this);
providers.put(hash, provider);
}
}
return provider;
}
@Override @Override
public <T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel) { public <T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel) {
String modelId = componentModel.getId(); String modelId = componentModel.getId();
@ -468,6 +505,9 @@ public class DefaultKeycloakSession implements KeycloakSession {
}; };
providers.values().forEach(safeClose); providers.values().forEach(safeClose);
closable.forEach(safeClose); closable.forEach(safeClose);
for (Entry<InvalidableObjectType, Set<Object>> me : invalidationMap.entrySet()) {
factory.invalidate(me.getKey(), me.getValue().toArray());
}
} }
} }

View file

@ -19,9 +19,13 @@ package org.keycloak.services;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentFactoryProvider;
import org.keycloak.component.ComponentFactoryProviderFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.KeycloakDeploymentInfo; import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEvent;
@ -34,6 +38,7 @@ import org.keycloak.provider.Spi;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.theme.DefaultThemeManagerFactory; import org.keycloak.theme.DefaultThemeManagerFactory;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -44,6 +49,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, ProviderManagerDeployer { public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, ProviderManagerDeployer {
@ -67,6 +73,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
private Long clientStorageProviderTimeout; private Long clientStorageProviderTimeout;
private Long roleStorageProviderTimeout; private Long roleStorageProviderTimeout;
protected ComponentFactoryProviderFactory componentFactoryPF;
@Override @Override
public void register(ProviderEventListener listener) { public void register(ProviderEventListener listener) {
@ -105,11 +112,18 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
} }
} }
checkProvider(); checkProvider();
// Component factory must be initialized first, so that postInit in other factories can use component factories
updateComponentFactoryProviderFactory();
if (componentFactoryPF != null) {
componentFactoryPF.postInit(this);
}
for (Map<String, ProviderFactory> factories : factoriesMap.values()) { for (Map<String, ProviderFactory> factories : factoriesMap.values()) {
for (ProviderFactory factory : factories.values()) { for (ProviderFactory factory : factories.values()) {
if (factory != componentFactoryPF) {
factory.postInit(this); factory.postInit(this);
} }
} }
}
// make the session factory ready for hot deployment // make the session factory ready for hot deployment
ProviderManagerRegistry.SINGLETON.setDeployer(this); ProviderManagerRegistry.SINGLETON.setDeployer(this);
} }
@ -150,12 +164,24 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
} }
factoriesMap = copy; factoriesMap = copy;
boolean cfChanged = false;
for (ProviderFactory factory : undeployed) { for (ProviderFactory factory : undeployed) {
invalidate(ObjectType.PROVIDER_FACTORY, factory.getClass());
factory.close(); factory.close();
cfChanged |= (componentFactoryPF == factory);
}
// Component factory must be initialized first, so that postInit in other factories can use component factories
if (cfChanged) {
updateComponentFactoryProviderFactory();
if (componentFactoryPF != null) {
componentFactoryPF.postInit(this);
}
} }
for (ProviderFactory factory : deployed) { for (ProviderFactory factory : deployed) {
if (factory != componentFactoryPF) {
factory.postInit(this); factory.postInit(this);
} }
}
if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) { if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) {
themeManagerFactory.clearCache(); themeManagerFactory.clearCache();
@ -314,6 +340,23 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
return map.get(id); return map.get(id);
} }
@Override
public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String realmId, String componentId, Function<KeycloakSessionFactory, ComponentModel> modelGetter) {
return (this.componentFactoryPF == null)
? null
: this.componentFactoryPF.getProviderFactory(clazz, realmId, componentId, modelGetter);
}
@Override
public void invalidate(InvalidableObjectType type, Object... ids) {
factoriesMap.values().stream()
.map(Map::values)
.flatMap(Collection::stream)
.filter(InvalidationHandler.class::isInstance)
.map(InvalidationHandler.class::cast)
.forEach(ih -> ih.invalidate(type, ids));
}
@Override @Override
public Stream<ProviderFactory> getProviderFactoriesStream(Class<? extends Provider> clazz) { public Stream<ProviderFactory> getProviderFactoriesStream(Class<? extends Provider> clazz) {
if (factoriesMap == null) return Stream.empty(); if (factoriesMap == null) return Stream.empty();
@ -379,4 +422,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
return serverStartupTimestamp; return serverStartupTimestamp;
} }
protected void updateComponentFactoryProviderFactory() {
this.componentFactoryPF = (ComponentFactoryProviderFactory) getProviderFactory(ComponentFactoryProvider.class);
}
} }

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.services.DefaultComponentFactoryProviderFactory

View file

@ -0,0 +1,31 @@
/*
* 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.testsuite.components.amphibian;
import org.keycloak.provider.Provider;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface TestAmphibianProvider extends Provider {
Map<String, Object> getDetails();
}

View file

@ -0,0 +1,26 @@
/*
* 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.testsuite.components.amphibian;
import org.keycloak.component.AmphibianProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface TestAmphibianProviderFactory<T extends TestAmphibianProvider> extends AmphibianProviderFactory<T> {
}

View file

@ -0,0 +1,117 @@
/*
* 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.testsuite.components.amphibian;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
public class TestAmphibianProviderFactoryImpl implements TestAmphibianProviderFactory {
public static final String PROVIDER_ID = "test";
private static final List<ProviderConfigProperty> CONFIG = ProviderConfigurationBuilder.create()
.property("secret", "Secret", "A secret value", STRING_TYPE, null, null, true)
.property("number", "Number", "A number value", STRING_TYPE, null, null, false)
.property("required", "Required", "A required value", STRING_TYPE, null, null, false)
.property("val1", "Value 1", "Some more values", STRING_TYPE, null, null, false)
.property("val2", "Value 2", "Some more values", STRING_TYPE, null, null, false)
.property("val3", "Value 3", "Some more values", STRING_TYPE, null, null, false)
.build();
private String secret;
private Integer number;
private String required;
private String val1;
private String val2;
private String val3;
@Override
public TestImplProvider create(KeycloakSession session) {
return new TestImplProvider();
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
ConfigurationValidationHelper.check(model)
.checkRequired("required", "Required")
.checkInt("number", "Number", false);
}
@Override
public String getHelpText() {
return "Provider to test component invalidation";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG;
}
@Override
public void init(Config.Scope config) {
this.secret = config.get("secret");
this.number = config.getInt("number");
this.required = config.get("required");
this.val1 = config.get("val1");
this.val2 = config.get("val2");
this.val3 = config.get("val3");
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
public class TestImplProvider implements TestAmphibianProvider {
@Override
public Map<String, Object> getDetails() {
Map<String, Object> c = new HashMap<>();
c.put("secret", secret);
c.put("number", number);
c.put("required", required);
c.put("val1", val1);
c.put("val2", val2);
c.put("val3", val3);
return c;
}
@Override
public void close() {
}
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.testsuite.components.amphibian;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TestAmphibianSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "test-amphibian";
}
@Override
public Class<? extends Provider> getProviderClass() {
return TestAmphibianProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return TestAmphibianProviderFactory.class;
}
}

View file

@ -62,6 +62,7 @@ import org.keycloak.services.util.CookieHelper;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.components.TestProvider; import org.keycloak.testsuite.components.TestProvider;
import org.keycloak.testsuite.components.TestProviderFactory; import org.keycloak.testsuite.components.TestProviderFactory;
import org.keycloak.testsuite.components.amphibian.TestAmphibianProvider;
import org.keycloak.testsuite.events.TestEventsListenerProvider; import org.keycloak.testsuite.events.TestEventsListenerProvider;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
import org.keycloak.testsuite.forms.PassThroughAuthenticator; import org.keycloak.testsuite.forms.PassThroughAuthenticator;
@ -690,6 +691,20 @@ public class TestingResourceProvider implements RealmResourceProvider {
})); }));
} }
@GET
@Path("/test-amphibian-component")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Map<String, Object>> getTestAmphibianComponentDetails() {
RealmModel realm = session.getContext().getRealm();
return realm.getComponentsStream(realm.getId(), TestAmphibianProvider.class.getName())
.collect(Collectors.toMap(
ComponentModel::getName,
componentModel -> {
TestAmphibianProvider t = session.getComponentProvider(TestAmphibianProvider.class, componentModel.getId());
return t == null ? null : t.getDetails();
}));
}
@GET @GET
@Path("/identity-config") @Path("/identity-config")
@ -993,7 +1008,6 @@ public class TestingResourceProvider implements RealmResourceProvider {
} }
private RealmModel getRealmByName(String realmName) { private RealmModel getRealmByName(String realmName) {
RealmProvider realmProvider = session.getProvider(RealmProvider.class); RealmProvider realmProvider = session.getProvider(RealmProvider.class);
RealmModel realm = realmProvider.getRealmByName(realmName); RealmModel realm = realmProvider.getRealmByName(realmName);

View file

@ -16,4 +16,5 @@
# #
org.keycloak.testsuite.domainextension.spi.ExampleSpi org.keycloak.testsuite.domainextension.spi.ExampleSpi
org.keycloak.testsuite.components.amphibian.TestAmphibianSpi
org.keycloak.testsuite.components.TestSpi org.keycloak.testsuite.components.TestSpi

View file

@ -0,0 +1,18 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.testsuite.components.amphibian.TestAmphibianProviderFactoryImpl

View file

@ -23,6 +23,7 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration {
private int bindHttpPort = 8080; private int bindHttpPort = 8080;
private int bindHttpsPortOffset = 0; private int bindHttpsPortOffset = 0;
private int bindHttpsPort = Integer.valueOf(System.getProperty("auth.server.https.port", "8543")); private int bindHttpsPort = Integer.valueOf(System.getProperty("auth.server.https.port", "8543"));
private int debugPort = -1;
private Path providersPath = Paths.get(System.getProperty("auth.server.home")); private Path providersPath = Paths.get(System.getProperty("auth.server.home"));
private int startupTimeoutInSeconds = 300; private int startupTimeoutInSeconds = 300;
private String route; private String route;
@ -145,4 +146,13 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration {
public void setReaugmentBeforeStart(boolean reaugmentBeforeStart) { public void setReaugmentBeforeStart(boolean reaugmentBeforeStart) {
this.reaugmentBeforeStart = reaugmentBeforeStart; this.reaugmentBeforeStart = reaugmentBeforeStart;
} }
public int getDebugPort() {
return debugPort;
}
public void setDebugPort(int debugPort) {
this.debugPort = debugPort;
}
} }

View file

@ -141,7 +141,10 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
commands.add("./kc.sh"); commands.add("./kc.sh");
if (Boolean.valueOf(System.getProperty("auth.server.debug", "false"))) { if (configuration.getDebugPort() > 0) {
commands.add("--debug");
commands.add(Integer.toString(configuration.getDebugPort()));
} else if (Boolean.valueOf(System.getProperty("auth.server.debug", "false"))) {
commands.add("--debug"); commands.add("--debug");
commands.add(System.getProperty("auth.server.debug.port", "5005")); commands.add(System.getProperty("auth.server.debug.port", "5005"));
} }

View file

@ -268,6 +268,11 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
Map<String, TestProvider.DetailsRepresentation> getTestComponentDetails(); Map<String, TestProvider.DetailsRepresentation> getTestComponentDetails();
@GET
@Path("/test-amphibian-component")
@Produces(MediaType.APPLICATION_JSON)
Map<String, Map<String, Object>> getTestAmphibianComponentDetails();
@GET @GET
@Path("/identity-config") @Path("/identity-config")

View file

@ -0,0 +1,198 @@
package org.keycloak.testsuite.cluster;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Before;
import org.keycloak.admin.client.resource.ComponentResource;
import org.keycloak.admin.client.resource.ComponentsResource;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.components.amphibian.TestAmphibianProvider;
import org.keycloak.testsuite.components.amphibian.TestAmphibianProviderFactoryImpl;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertNull;
/**
*
* @author tkyjovsk
*/
public class ComponentInvalidationClusterTest extends AbstractInvalidationClusterTestWithTestRealm<ComponentRepresentation, ComponentResource> {
@Before
public void setExcludedComparisonFields() {
}
@Override
protected ComponentRepresentation createTestEntityRepresentation() {
ComponentRepresentation comp = new ComponentRepresentation();
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
comp.setName("comp_" + RandomStringUtils.randomAlphabetic(5));
comp.setProviderId(TestAmphibianProviderFactoryImpl.PROVIDER_ID);
comp.setProviderType(TestAmphibianProvider.class.getName());
config.putSingle("secret", "Secret");
config.putSingle("required", "required-value");
config.putSingle("number", "2");
config.put("val1", Arrays.asList(new String[]{"val1 value"}));
config.put("val2", Arrays.asList(new String[]{"val2 value"}));
comp.setConfig(config);
return comp;
}
protected ComponentsResource components(ContainerInfo node) {
return getAdminClientFor(node).realm(testRealmName).components();
}
@Override
protected ComponentResource entityResource(ComponentRepresentation comp, ContainerInfo node) {
return entityResource(comp.getId(), node);
}
@Override
protected ComponentResource entityResource(String id, ContainerInfo node) {
return components(node).component(id);
}
@Override
protected ComponentRepresentation createEntity(ComponentRepresentation comp, ContainerInfo node) {
comp.setParentId(getAdminClientFor(node).realm(testRealmName).toRepresentation().getId());
try (Response response = components(node).add(comp)) {
String id = ApiUtil.getCreatedId(response);
comp.setId(id);
}
return readEntity(comp, node);
}
@Override
protected ComponentRepresentation readEntity(ComponentRepresentation comp, ContainerInfo node) {
ComponentRepresentation u = null;
try {
u = entityResource(comp, node).toRepresentation();
} catch (NotFoundException nfe) {
// expected when component doesn't exist
}
return u;
}
@Override
protected ComponentRepresentation updateEntity(ComponentRepresentation comp, ContainerInfo node) {
entityResource(comp, node).update(comp);
return readEntity(comp, node);
}
@Override
protected void deleteEntity(ComponentRepresentation comp, ContainerInfo node) {
entityResource(comp, node).remove();
assertNull(readEntity(comp, node));
}
@Override
protected ComponentRepresentation testEntityUpdates(ComponentRepresentation comp, boolean backendFailover) {
comp.setName(comp.getName() + "_updated");
comp = updateEntityOnCurrentFailNode(comp, "name");
verifyEntityUpdateDuringFailover(comp, backendFailover);
// config - add new
comp.getConfig().putSingle("val3", "val3 value");
comp = updateEntityOnCurrentFailNode(comp, "config - adding");
verifyEntityUpdateDuringFailover(comp, backendFailover);
// config - remove
comp.getConfig().remove("val3");
comp = updateEntityOnCurrentFailNode(comp, "config - removing");
verifyEntityUpdateDuringFailover(comp, backendFailover);
// config - update 1
comp.getConfig().get("val1").set(0,
comp.getConfig().get("val1").get(0) + " - updated");
comp = updateEntityOnCurrentFailNode(comp, "config");
verifyEntityUpdateDuringFailover(comp, backendFailover);
return comp;
}
@Test
public void testComponentUpdating() {
ComponentRepresentation testEntity = createTestEntityRepresentation();
// CREATE
log.info("(1) createEntityOnCurrentFailNode");
ComponentRepresentation comp = createEntityOnCurrentFailNode(testEntity);
for (ContainerInfo ci : suiteContext.getAuthServerBackendsInfo()) {
assertComponentHasCorrectConfig(comp, ci);
}
iterateCurrentFailNode();
// config - add new
comp.getConfig().putSingle("val3", "val3 value");
comp = updateEntityOnCurrentFailNode(comp, "config - adding");
for (ContainerInfo ci : suiteContext.getAuthServerBackendsInfo()) {
assertComponentHasCorrectConfig(comp, ci);
}
iterateCurrentFailNode();
// config - remove
comp.getConfig().remove("val3");
comp = updateEntityOnCurrentFailNode(comp, "config - removing");
for (ContainerInfo ci : suiteContext.getAuthServerBackendsInfo()) {
assertComponentHasCorrectConfig(comp, ci);
}
iterateCurrentFailNode();
// config - update 1
comp.getConfig().get("val1").set(0,
comp.getConfig().get("val1").get(0) + " - updated");
comp = updateEntityOnCurrentFailNode(comp, "config");
for (ContainerInfo ci : suiteContext.getAuthServerBackendsInfo()) {
assertComponentHasCorrectConfig(comp, ci);
}
}
@Override
protected void assertEntityOnSurvivorNodesEqualsTo(ComponentRepresentation testEntityOnFailNode) {
super.assertEntityOnSurvivorNodesEqualsTo(testEntityOnFailNode);
for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) {
assertComponentHasCorrectConfig(testEntityOnFailNode, survivorNode);
}
}
protected void assertComponentHasCorrectConfig(ComponentRepresentation testEntityOnFailNode, ContainerInfo survivorNode) throws NumberFormatException {
log.debug(String.format("Attempt to verify %s component reinstantiation on %s (%s)", getEntityType(testEntityOnFailNode), survivorNode, survivorNode.getContextRoot()));
Map<String, Map<String, Object>> config = getTestingClientFor(survivorNode).testing(testRealmName).getTestAmphibianComponentDetails();
assertThat(config, hasKey(testEntityOnFailNode.getName()));
Map<String, Object> c = config.get(testEntityOnFailNode.getName());
assertThat(c, hasEntry("number", Integer.valueOf(testEntityOnFailNode.getConfig().getFirst("number"))));
assertThat(c, hasEntry("required", testEntityOnFailNode.getConfig().getFirst("required")));
assertThat(c, hasEntry("val1", testEntityOnFailNode.getConfig().getFirst("val1")));
assertThat(c, hasEntry("val2", testEntityOnFailNode.getConfig().getFirst("val2")));
final Object val3 = testEntityOnFailNode.getConfig().getFirst("val3");
if (val3 == null) {
assertThat(c, anyOf(hasEntry("val3", null), not(hasKey("val3"))));
} else {
assertThat(c, hasEntry("val3", val3));
}
}
}

View file

@ -643,6 +643,7 @@
<property name="route">node1</property> <property name="route">node1</property>
<property name="remoteMode">${quarkus.remote}</property> <property name="remoteMode">${quarkus.remote}</property>
<property name="profile">ha</property> <property name="profile">ha</property>
<property name="debugPort">5005</property>
<property name="keycloakConfigPropertyOverrides">{ <property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node1", "keycloak.connectionsInfinispan.nodeName": "node1",
@ -650,6 +651,7 @@
} }
</property> </property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property> <property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>
<property name="outputToConsole">true</property>
</configuration> </configuration>
</container> </container>
<container qualifier="auth-server-quarkus-backend2" mode="manual" > <container qualifier="auth-server-quarkus-backend2" mode="manual" >
@ -664,6 +666,7 @@
<property name="route">node2</property> <property name="route">node2</property>
<property name="remoteMode">${quarkus.remote}</property> <property name="remoteMode">${quarkus.remote}</property>
<property name="profile">ha</property> <property name="profile">ha</property>
<property name="debugPort">5006</property>
<property name="keycloakConfigPropertyOverrides">{ <property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node2", "keycloak.connectionsInfinispan.nodeName": "node2",
@ -671,6 +674,7 @@
} }
</property> </property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property> <property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>
<property name="outputToConsole">true</property>
</configuration> </configuration>
</container> </container>
</group> </group>

View file

@ -40,6 +40,7 @@ log4j.logger.org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBal
# Enable to view loaded SPI and Providers # Enable to view loaded SPI and Providers
# log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug # log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
# log4j.logger.org.keycloak.services.DefaultComponentFactoryProviderFactory=debug
# log4j.logger.org.keycloak.provider.ProviderManager=debug # log4j.logger.org.keycloak.provider.ProviderManager=debug
# log4j.logger.org.keycloak.provider.FileSystemProviderLoaderFactory=debug # log4j.logger.org.keycloak.provider.FileSystemProviderLoaderFactory=debug