8000 WIP - Add support for configuration profiles by sguilhen · Pull Request #10429 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion core/src/main/java/org/keycloak/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak;

import java.util.HashSet;
import java.util.Set;

/**
Expand Down Expand Up @@ -158,7 +159,16 @@ public Scope scope(String... scope) {

@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("Not implemented");
Set<String> propertyNames = new HashSet<>();
for (Object key : System.getProperties().keySet()) {
String keyString = (String) key;
if (keyString.startsWith(this.prefix)) {
String suffix = keyString.substring(this.prefix.length());
int dotPosition = suffix.indexOf(".");
propertyNames.add(suffix.substring(0, dotPosition == -1 ? suffix.length() : dotPosition));
}
}
return propertyNames;
}

}
Expand Down
122 changes: 122 additions & 0 deletions core/src/main/java/org/keycloak/util/ConfigUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.keycloak.util;

import java.util.HashSet;
import java.util.Set;

import org.keycloak.Config;

import static org.keycloak.Config.Scope;

public class ConfigUtil {

public static class CompositeScope implements Scope {

private Scope[] scopes;

public CompositeScope(Scope... scopes) {
this.scopes = scopes == null ? new Scope[0] : scopes;
}

@Override
public String get(String key) {
return this.get(key, null);
}

@Override
public String get(String key, String defaultValue) {
for (Scope scope : this.scopes) {
String value = scope.get(key);
if (value != null) {
return value;
}
}
return defaultValue;
}

@Override
public String[] getArray(String key) {
for (Scope scope : this.scopes) {
String[] value = scope.getArray(key);
if (value != null) {
return value;
}
}
return null;
}

@Override
public Integer getInt(String key) {
return this.getInt(key, null);
}

@Override
public Integer getInt(String key, Integer defaultValue) {
for (Scope scope : this.scopes) {
Integer value = scope.getInt(key);
if (value != null) {
return value;
}
}
return defaultValue;
}

@Override
public Long getLong(String key) {
return this.getLong(key, null);
}

@Override
public Long getLong(String key, Long defaultValue) {
for (Scope scope : this.scopes) {
Long value = scope.getLong(key);
if (value != null) {
return value;
}
}
return defaultValue;
}

@Override
public Boolean getBoolean(String key) {
return this.getBoolean(key, null);
}

@Override
public Boolean getBoolean(String key, Boolean defaultValue) {
for (Scope scope : this.scopes) {
Boolean value = scope.getBoolean(key);
if (value != null) {
return value;
}
}
return defaultValue;
}

@Override
public Scope scope(String... scope) {
Scope[] subScopes = new Scope[this.scopes.length];
for (int i = 0; i < this.scopes.length; i++) {
subScopes[i] = this.scopes[i].scope(scope);
}
return new CompositeScope(subScopes);
}

@Override
public Set<String> getPropertyNames() {
Set<String> propertyNames = new HashSet<>();
for (Scope scope : this.scopes) {
Set<String> scopePropertyNames = scope.getPropertyNames();
if (scopePropertyNames != null) {
propertyNames.addAll(scopePropertyNames);
}
}
return propertyNames;
}
}

public static Scope getProviderScope(final String spiName, final String provider, final String profile) {
Scope rootScope = Config.scope(spiName, provider);
Scope profileScope = rootScope.scope("profile", profile);
return new CompositeScope(profileScope, rootScope);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.keycloak.common.util.StackUtil;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ProfileEnabledProviderFactory;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
Expand Down Expand Up @@ -79,7 +80,8 @@
public class JpaMapStorageProviderFactory implements
AmphibianProviderFactory<MapStorageProvider>,
MapStorageProviderFactory,
EnvironmentDependentProviderFactory {
EnvironmentDependentProviderFactory,
ProfileEnabledProviderFactory {

public static final String PROVIDER_ID = "jpa-map-storage";
private static final Logger logger = Logger.getLogger(JpaMapStorageProviderFactory.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.AuthorizationStoreFactory;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.KeycloakSession;
Expand Down Expand Up @@ -54,7 +55,7 @@ public class MapAuthorizationStoreFactory implements AmphibianProviderFactory<St
@Override
public StoreFactory create(KeycloakSession session) {
MapStorageProviderFactory storageProviderFactory = (MapStorageProviderFactory) getComponentFactory(session.getKeycloakSessionFactory(),
MapStorageProvider.class, storageConfigScope, MapStorageSpi.NAME);
MapStorageProvider.class, storageConfigScope, MapStorageSpi.NAME, "authorizationPersister");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this constant originate from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it is coming from the SPI name. For authorization the different areas are not currently separated, they are all initialized to use the same storage factory. I don't know if we will change that in the future so that the different authz models will be configurable independently with respect to the storage factory, but for now they all share the same so I decided to use the SPI name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we won't be splitting the different authz models, then perhaps a better name for the authz storage would be simply authorization.

final MapStorageProvider mapStorageProvider = storageProviderFactory.create(session);
AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.map.storage.ModelEntityUtil;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
Expand All @@ -46,7 +47,6 @@ public abstract class AbstractMapProviderFactory<T extends Provider, V extends A

private Scope storageConfigScope;

@SuppressWarnings("unchecked")
protected AbstractMapProviderFactory(Class<M> modelType) {
this.modelType = modelType;
}
Expand All @@ -58,7 +58,7 @@ public String getId() {

protected MapStorage<V, M> getStorage(KeycloakSession session) {
ProviderFactory<MapStorageProvider> storageProviderFactory = getComponentFactory(session.getKeycloakSessionFactory(),
MapStorageProvider.class, storageConfigScope, MapStorageSpi.NAME);
MapStorageProvider.class, storageConfigScope, MapStorageSpi.NAME, ModelEntityUtil.getModelName(this.modelType));
final MapStorageProvider factory = storageProviderFactory.create(session);

return factory.getStorage(modelType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ public void loadPersistentSessions(KeycloakSessionFactory sessionFactory, int ma
@Override
public MapUserSessionProvider create(KeycloakSession session) {
MapStorageProviderFactory storageProviderFactoryUs = (MapStorageProviderFactory) getComponentFactory(session.getKeycloakSessionFactory(),
MapStorageProvider.class, storageConfigScopeUserSessions, MapStorageSpi.NAME);
MapStorageProvider.class, storageConfigScopeUserSessions, MapStorageSpi.NAME, UserSessionModel.class.getSimpleName());
final MapStorageProvider factoryUs = storageProviderFactoryUs.create(session);
MapStorage userSessionStore = factoryUs.getStorage(UserSessionModel.class);

MapStorageProviderFactory storageProviderFactoryCs = (MapStorageProviderFactory) getComponentFactory(session.getKeycloakSessionFactory(),
MapStorageProvider.class, storageConfigScopeClientSessions, MapStorageSpi.NAME);
MapStorageProvider.class, storageConfigScopeClientSessions, MapStorageSpi.NAME, AuthenticatedClientSessionModel.class.getSimpleName());
final MapStorageProvider factoryCs = storageProviderFactoryCs.create(session);
MapStorage clientSessionStore = factoryCs.getStorage(AuthenticatedClientSessionModel.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.keycloak.component;

import java.util.HashSet;
import java.util.Set;
import org.keycloak.Config.Scope;

Expand Down Expand Up @@ -116,7 +117,16 @@ public Scope scope(String... scope) {

@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("Not implemented");
Set<String> keys = new HashSet<>();
for (String key : this.componentConfig.getConfig().keySet()) {
if (key.startsWith(this.prefix)) {
String suffix = key.substring(this.prefix.length());
int dotPosition = suffix.indexOf(".");
keys.add(suffix.substring(0, dotPosition == -1 ? suffix.length() : dotPosition));
}
}
keys.addAll(this.origScope.getPropertyNames());
return keys;
}

public ComponentModel getComponentModel() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.component;

/**
* Marker interface to identify provider factories that support multiple configuration profiles.
*/
public interface ProfileEnabledProviderFactory {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose the introduction of this interface to allow the code that is initializing the instantiated factories to handle profiles only if the factory supports them, for efficiency reasons. But, of course, this can be changed.

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
import org.keycloak.models.AccountRoles;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.util.ConfigUtil;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

Expand Down Expand Up @@ -304,12 +306,13 @@ public static ComponentModel getComponentModel(KeycloakSessionFactory factory, S
return cm.get();
}

public static <T extends Provider> ProviderFactory<T> getComponentFactory(KeycloakSessionFactory factory, Class<T> providerClass, Scope config, String spiName) {
public static <T extends Provider> ProviderFactory<T> getComponentFactory(final KeycloakSessionFactory factory, final Class<T> providerClass,
final Scope config, final String spiName, final String modelName) {
String realmId = config.get("realmId");
String componentId = config.get("componentId");
if (realmId == null || componentId == null) {
realmId = "ROOT";
ComponentModel cm = new ScopeComponentModel(providerClass, config, spiName, realmId);
ComponentModel cm = new ScopeComponentModel(providerClass, config, spiName, realmId, modelName);
return factory.getProviderFactory(providerClass, realmId, cm.getId(), k -> cm);
} else {
return factory.getProviderFactory(providerClass, realmId, componentId, componentModelGetter(realmId, componentId));
Expand All @@ -321,17 +324,21 @@ private static class ScopeComponentModel extends ComponentModel {
private final String componentId;
private final String providerId;
private final String providerType;
private final String providerProfile;
private final String realmId;
private final Scope config;

public ScopeComponentModel(Class<?> providerClass, Scope baseConfiguration, String spiName, String realmId) {
final String pr = baseConfiguration.get("provider", Config.getProvider(spiName));

this.providerId = pr == null ? "default" : pr;
public ScopeComponentModel(final Class<?> providerClass, final Scope baseConfiguration, final String spiName, final String realmId,
final String modelName) {
String str = baseConfiguration.get("provider", Config.getProvider(spiName));
String[] providerAndProfile = (str != null) ? str.split("/") : new String[]{"default"};
this.providerId = providerAndProfile[0];
this.providerType = providerClass.getName();
this.providerProfile = providerAndProfile.length > 1 ? providerAndProfile[1] : "default";
this.config = baseConfiguration.scope(this.providerId);
this.componentId = spiName + "- " + (realmId == null ? "" : "f:" + realmId + ":") + this.providerId;
this.componentId = spiName + "-" + (realmId == null ? "" : "f:" + realmId + ":") + this.providerId + ":" +
(this.config.getPropertyNames().isEmpty() ? this.providerProfile : modelName);
this.realmId = realmId;
this.providerType = providerClass.getName();
}

@Override
Expand Down Expand Up @@ -376,6 +383,9 @@ public int get(String key, int defaultValue) {

@Override
public String get(String key, String defaultValue) {
if ("provider-profile".equals(key)) {
return this.providerProfile;
}
return config.get(key, defaultValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.keycloak.component.ComponentFactoryProviderFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentModelScope;
import org.keycloak.component.ProfileEnabledProviderFactory;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.InvalidationHandler;
Expand All @@ -40,6 +41,7 @@
import java.util.function.Function;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.util.ConfigUtil;

/**
* @author hmlnarik
Expand Down Expand Up @@ -124,7 +126,9 @@ public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz
return null;
}

Scope scope = Config.scope(factory.getSpi(clazz).getName(), provider);
Scope scope = (newFactory instanceof ProfileEnabledProviderFactory) ?
ConfigUtil.getProviderScope(factory.getSpi(clazz).getName(), provider, cm.get("provider-profile")) :
Config.scope(factory.getSpi(clazz).getName(), provider);
ComponentModelScope configScope = new ComponentModelScope(scope, cm);

ProviderFactory<T> providerFactory;
Expand Down
Loading
0