KEYCLOAK-17409 Support for amphibian (both component and standalone) provider
This commit is contained in:
parent
5fac80b05e
commit
a36fafe04e
29 changed files with 1318 additions and 8 deletions
|
@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,16 @@ 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()) {
|
||||||
factory.postInit(this);
|
if (factory != componentFactoryPF) {
|
||||||
|
factory.postInit(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {};
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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>}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +112,16 @@ 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()) {
|
||||||
factory.postInit(this);
|
if (factory != componentFactoryPF) {
|
||||||
|
factory.postInit(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// make the session factory ready for hot deployment
|
// make the session factory ready for hot deployment
|
||||||
|
@ -150,11 +164,23 @@ 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) {
|
||||||
factory.postInit(this);
|
if (factory != componentFactoryPF) {
|
||||||
|
factory.postInit(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) {
|
if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue