KEYCLOAK-14812 Create RoleStorageManager

This commit is contained in:
vramik 2020-07-21 12:34:50 +02:00 committed by Pedro Igor
parent bfa21c912c
commit 6b00633c47
17 changed files with 1180 additions and 65 deletions

View file

@ -160,8 +160,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public RoleProvider getRoleDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (roleDelegate != null) return roleDelegate;
// roleDelegate = session.roleStorageManager();
roleDelegate = session.roleLocalStorage();
roleDelegate = session.roleStorageManager();
return roleDelegate;
}

View file

@ -0,0 +1,109 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.role;
import org.keycloak.Config;
import org.keycloak.component.ComponentFactory;
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.ProviderConfigProperty;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public interface RoleStorageProviderFactory<T extends RoleStorageProvider> extends ComponentFactory<T, RoleStorageProvider> {
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
@Override
T create(KeycloakSession session, ComponentModel model);
/**
* This is the name of the provider.
*
* @return
*/
@Override
String getId();
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
@Override
default String getHelpText() {
return "";
}
@Override
default List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}
/**
* Called when RoleStorageProviderModel is created. This allows you to do initialization of any additional configuration
* you need to add.
*
* @param session
* @param realm
* @param model
*/
@Override
default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
/**
* configuration properties that are common across all RoleStorageProvider implementations
*
* @return
*/
@Override
default
List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return RoleStorageProviderSpi.commonConfig();
}
@Override
default
Map<String, Object> getTypeMetadata() {
return new HashMap<>();
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.role;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import java.util.Collections;
import java.util.List;
public class RoleStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "role-storage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return RoleStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return RoleStorageProviderFactory.class;
}
private static final List<ProviderConfigProperty> commonConfig;
static {
//corresponds to properties defined in CacheableStorageProviderModel and PrioritizedComponentModel
List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
.property()
.name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
.property()
.name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add()
.build();
commonConfig = Collections.unmodifiableList(config);
}
public static List<ProviderConfigProperty> commonConfig() {
return commonConfig;
}
}

View file

@ -74,6 +74,7 @@ org.keycloak.credential.CredentialSpi
org.keycloak.keys.PublicKeyStorageSpi
org.keycloak.keys.KeySpi
org.keycloak.storage.client.ClientStorageProviderSpi
org.keycloak.storage.role.RoleStorageProviderSpi
org.keycloak.crypto.SignatureSpi
org.keycloak.crypto.ClientSignatureVerifierSpi
org.keycloak.crypto.HashSpi

View file

@ -157,7 +157,10 @@ public interface KeycloakSession {
ClientProvider clientStorageManager();
// RoleProvider roleStorageManager();
/**
* @return RoleStorageManager instance
*/
RoleProvider roleStorageManager();
/**
* Un-cached view of all users in system including users loaded by UserStorageProviders
@ -190,7 +193,7 @@ public interface KeycloakSession {
ClientProvider clientLocalStorage();
/**
* Keycloak specific local storage for roles. No cache in front, this api talks directly to database configured for Keycloak
* Keycloak specific local storage for roles. No cache in front, this api talks directly to storage configured for Keycloak
*
* @return
*/

View file

@ -24,6 +24,8 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.client.ClientStorageProvider;
import org.keycloak.storage.client.ClientStorageProviderModel;
import org.keycloak.storage.role.RoleStorageProvider;
import org.keycloak.storage.role.RoleStorageProviderModel;
import java.util.*;
import java.util.stream.Collectors;
@ -441,6 +443,16 @@ public interface RealmModel extends RoleContainerModel {
return list;
}
default
List<RoleStorageProviderModel> getRoleStorageProviders() {
List<RoleStorageProviderModel> list = new LinkedList<>();
for (ComponentModel component : getComponents(getId(), RoleStorageProvider.class.getName())) {
list.add(new RoleStorageProviderModel(component));
}
Collections.sort(list, RoleStorageProviderModel.comparator);
return list;
}
String getLoginTheme();
void setLoginTheme(String name);

View file

@ -20,12 +20,13 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.provider.Provider;
import org.keycloak.storage.role.RoleLookupProvider;
/**
* Provider of the role records.
* @author vramik
*/
public interface RoleProvider extends Provider {
public interface RoleProvider extends Provider, RoleLookupProvider {
/**
* Adds a realm role with given {@code name} to the given realm.
@ -136,51 +137,4 @@ public interface RoleProvider extends Provider {
* @param client Client.
*/
void removeRoles(ClientModel client);
//TODO RoleLookupProvider
/**
* Exact search for a role by given name.
* @param realm Realm.
* @param name String name of the role.
* @return Model of the role, or {@code null} if no role is found.
*/
RoleModel getRealmRole(RealmModel realm, String name);
/**
* Exact search for a role by its internal ID..
* @param realm Realm.
* @param id Internal ID of the role.
* @return Model of the role.
*/
RoleModel getRoleById(RealmModel realm, String id);
/**
* Case-insensitive search for roles that contain the given string in their name or description.
* @param realm Realm.
* @param search Searched substring of the role's name or description.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the realm roles their name or description contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max);
/**
* Exact search for a client role by given name.
* @param client Client.
* @param name String name of the role.
* @return Model of the role, or {@code null} if no role is found.
*/
RoleModel getClientRole(ClientModel client, String name);
/**
* Case-insensitive search for client roles that contain the given string in their name or description.
* @param client Client.
* @param search String to search by role's name or description.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the client roles their name or description contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max);
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.role;
import java.util.stream.Stream;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
/**
* Abstraction interface for lookup of both realm roles and client roles by id, name and description.
*/
public interface RoleLookupProvider {
/**
* Exact search for a role by given name.
* @param realm Realm.
* @param name String name of the role.
* @return Model of the role, or {@code null} if no role is found.
*/
RoleModel getRealmRole(RealmModel realm, String name);
/**
* Exact search for a role by its internal ID..
* @param realm Realm.
* @param id Internal ID of the role.
* @return Model of the role.
*/
RoleModel getRoleById(RealmModel realm, String id);
/**
* Case-insensitive search for roles that contain the given string in their name or description.
* @param realm Realm.
* @param search Searched substring of the role's name or description.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the realm roles their name or description contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max);
/**
* Exact search for a client role by given name.
* @param client Client.
* @param name String name of the role.
* @return Model of the role, or {@code null} if no role is found.
*/
RoleModel getClientRole(ClientModel client, String name);
/**
* Case-insensitive search for client roles that contain the given string in their name or description.
* @param client Client.
* @param search String to search by role's name or description.
* @param first First result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of the client roles their name or description contains given search string.
* Never returns {@code null}.
*/
Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.role;
import org.keycloak.provider.Provider;
/**
* Base interface for components that want to provide an alternative storage mechanism for roles
*/
public interface RoleStorageProvider extends Provider, RoleLookupProvider {
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.role;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.CacheableStorageProviderModel;
/**
* Stored configuration of a Role Storage provider instance.
*/
public class RoleStorageProviderModel extends CacheableStorageProviderModel {
public RoleStorageProviderModel() {
setProviderType(RoleStorageProvider.class.getName());
}
public RoleStorageProviderModel(ComponentModel copy) {
super(copy);
}
private transient Boolean enabled;
@Override
public void setEnabled(boolean flag) {
enabled = flag;
getConfig().putSingle(ENABLED, Boolean.toString(flag));
}
@Override
public boolean isEnabled() {
if (enabled == null) {
String val = getConfig().getFirst(ENABLED);
if (val == null) {
enabled = true;
} else {
enabled = Boolean.valueOf(val);
}
}
return enabled;
}
}

View file

@ -42,7 +42,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyManager;
import org.keycloak.services.clientpolicy.DefaultClientPolicyManager;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.ClientStorageManager;
//import org.keycloak.storage.RoleStorageManager;
import org.keycloak.storage.RoleStorageManager;
import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.vault.DefaultVaultTranscriber;
@ -72,7 +72,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
private RoleProvider roleProvider;
private UserStorageManager userStorageManager;
private ClientStorageManager clientStorageManager;
// private RoleStorageManager roleStorageManager;
private RoleStorageManager roleStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
private UserSessionProvider sessionProvider;
private AuthenticationSessionProvider authenticationSessionProvider;
@ -120,8 +120,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
if (cache != null) {
return cache;
} else {
// return roleStorageManager();
return roleLocalStorage();
return roleStorageManager();
}
}
@ -204,11 +203,13 @@ public class DefaultKeycloakSession implements KeycloakSession {
return getProvider(RoleProvider.class);
}
// @Override
// public RoleProvider roleStorageManager() {
// if (roleStorageManager == null) roleStorageManager = new RoleStorageManager(this);
// return roleStorageManager;
// }
@Override
public RoleProvider roleStorageManager() {
if (roleStorageManager == null) {
roleStorageManager = new RoleStorageManager(this, factory.getRoleStorageProviderTimeout());
}
return roleStorageManager;
}
@Override

View file

@ -60,11 +60,13 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
protected long serverStartupTimestamp;
/**
* Timeout is used as time boundary for obtaining clients from an external client storage. Default value is set
* Timeouts are used as time boundary for obtaining models from an external storage. Default value is set
* to 3000 milliseconds and it's configurable.
*/
private long clientStorageProviderTimeout;
private Long clientStorageProviderTimeout;
private Long roleStorageProviderTimeout;
@Override
public void register(ProviderEventListener listener) {
listeners.add(listener);
@ -85,8 +87,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public void init() {
serverStartupTimestamp = System.currentTimeMillis();
clientStorageProviderTimeout = Config.scope("client").getLong("storageProviderTimeout", 3000L);
ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), getClass().getClassLoader(), Config.scope().getArray("providers"));
spis.addAll(pm.loadSpis());
factoriesMap = loadFactories(pm);
@ -360,9 +360,19 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
}
public long getClientStorageProviderTimeout() {
if (clientStorageProviderTimeout == null) {
clientStorageProviderTimeout = Config.scope("client").getLong("storageProviderTimeout", 3000L);
}
return clientStorageProviderTimeout;
}
public long getRoleStorageProviderTimeout() {
if (roleStorageProviderTimeout == null) {
roleStorageProviderTimeout = Config.scope("role").getLong("storageProviderTimeout", 3000L);
}
return roleStorageProviderTimeout;
}
/**
* @return timestamp of Keycloak server startup
*/

View file

@ -0,0 +1,249 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleProvider;
import org.keycloak.storage.role.RoleLookupProvider;
import org.keycloak.storage.role.RoleStorageProvider;
import org.keycloak.storage.role.RoleStorageProviderFactory;
import org.keycloak.storage.role.RoleStorageProviderModel;
import org.keycloak.utils.ServicesUtils;
public class RoleStorageManager implements RoleProvider {
private static final Logger logger = Logger.getLogger(RoleStorageManager.class);
protected KeycloakSession session;
private final long roleStorageProviderTimeout;
public RoleStorageManager(KeycloakSession session, long roleStorageProviderTimeout) {
this.session = session;
this.roleStorageProviderTimeout = roleStorageProviderTimeout;
}
public static boolean isStorageProviderEnabled(RealmModel realm, String providerId) {
RoleStorageProviderModel model = getStorageProviderModel(realm, providerId);
return model.isEnabled();
}
public static RoleStorageProviderModel getStorageProviderModel(RealmModel realm, String componentId) {
ComponentModel model = realm.getComponent(componentId);
if (model == null) return null;
return new RoleStorageProviderModel(model);
}
public static RoleStorageProvider getStorageProvider(KeycloakSession session, RealmModel realm, String componentId) {
ComponentModel model = realm.getComponent(componentId);
if (model == null) return null;
RoleStorageProviderModel storageModel = new RoleStorageProviderModel(model);
RoleStorageProviderFactory factory = (RoleStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(RoleStorageProvider.class, model.getProviderId());
if (factory == null) {
throw new ModelException("Could not find RoletStorageProviderFactory for: " + model.getProviderId());
}
return getStorageProviderInstance(session, storageModel, factory);
}
public static List<RoleStorageProviderModel> getStorageProviders(RealmModel realm) {
return realm.getRoleStorageProviders();
}
public static RoleStorageProvider getStorageProviderInstance(KeycloakSession session, RoleStorageProviderModel model, RoleStorageProviderFactory factory) {
RoleStorageProvider instance = (RoleStorageProvider)session.getAttribute(model.getId());
if (instance != null) return instance;
instance = factory.create(session, model);
if (instance == null) {
throw new IllegalStateException("RoleStorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance");
}
session.enlistForClose(instance);
session.setAttribute(model.getId(), instance);
return instance;
}
public static <T> List<T> getStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
List<T> list = new LinkedList<>();
for (RoleStorageProviderModel model : getStorageProviders(realm)) {
RoleStorageProviderFactory factory = (RoleStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(RoleStorageProvider.class, model.getProviderId());
if (factory == null) {
logger.warnv("Configured RoleStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName());
continue;
}
if (Types.supports(type, factory, RoleStorageProviderFactory.class)) {
list.add(type.cast(getStorageProviderInstance(session, model, factory)));
}
}
return list;
}
public static <T> List<T> getEnabledStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
List<T> list = new LinkedList<>();
for (RoleStorageProviderModel model : getStorageProviders(realm)) {
if (!model.isEnabled()) continue;
RoleStorageProviderFactory factory = (RoleStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(RoleStorageProvider.class, model.getProviderId());
if (factory == null) {
logger.warnv("Configured RoleStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName());
continue;
}
if (Types.supports(type, factory, RoleStorageProviderFactory.class)) {
list.add(type.cast(getStorageProviderInstance(session, model, factory)));
}
}
return list;
}
@Override
public RoleModel addRealmRole(RealmModel realm, String name) {
return session.roleLocalStorage().addRealmRole(realm, name);
}
@Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) {
return session.roleLocalStorage().addRealmRole(realm, id, name);
}
@Override
public RoleModel getRealmRole(RealmModel realm, String name) {
RoleModel realmRole = session.roleLocalStorage().getRealmRole(realm, name);
if (realmRole != null) return realmRole;
for (RoleLookupProvider enabledStorageProvider : getEnabledStorageProviders(session, realm, RoleLookupProvider.class)) {
realmRole = enabledStorageProvider.getRealmRole(realm, name);
if (realmRole != null) return realmRole;
}
return null;
}
@Override
public RoleModel getRoleById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
if (storageId.getProviderId() == null) {
return session.roleLocalStorage().getRoleById(realm, id);
}
RoleLookupProvider provider = (RoleLookupProvider)getStorageProvider(session, realm, storageId.getProviderId());
if (provider == null) return null;
if (! isStorageProviderEnabled(realm, storageId.getProviderId())) return null;
return provider.getRoleById(realm, id);
}
@Override
public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, Integer max) {
return session.roleLocalStorage().getRealmRolesStream(realm, first, max);
}
/**
* Obtaining roles from an external role storage is time-bounded. In case the external role storage
* isn't available at least roles from a local storage are returned. For this purpose
* the {@link org.keycloak.services.DefaultKeycloakSessionFactory#getRoleStorageProviderTimeout()} property is used.
* Default value is 3000 milliseconds and it's configurable.
* See {@link org.keycloak.services.DefaultKeycloakSessionFactory} for details.
*/
@Override
public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) {
Stream<RoleModel> local = session.roleLocalStorage().searchForRolesStream(realm, search, first, max);
Stream<RoleModel> ext = getEnabledStorageProviders(session, realm, RoleLookupProvider.class).stream()
.flatMap(ServicesUtils.timeBound(session,
roleStorageProviderTimeout,
p -> ((RoleLookupProvider) p).searchForRolesStream(realm, search, first, max)));
return Stream.concat(local, ext);
}
@Override
public boolean removeRole(RoleModel role) {
if (!StorageId.isLocalStorage(role.getId())) {
throw new RuntimeException("Federated roles do not support this operation");
}
return session.roleLocalStorage().removeRole(role);
}
@Override
public void removeRoles(RealmModel realm) {
session.roleLocalStorage().removeRoles(realm);
}
@Override
public void removeRoles(ClientModel client) {
session.roleLocalStorage().removeRoles(client);
}
@Override
public RoleModel addClientRole(ClientModel client, String name) {
return session.roleLocalStorage().addClientRole(client, name);
}
@Override
public RoleModel addClientRole(ClientModel client, String id, String name) {
return session.roleLocalStorage().addClientRole(client, id, name);
}
@Override
public RoleModel getClientRole(ClientModel client, String name) {
RoleModel clientRole = session.roleLocalStorage().getClientRole(client, name);
if (clientRole != null) return clientRole;
for (RoleLookupProvider enabledStorageProvider : getEnabledStorageProviders(session, client.getRealm(), RoleLookupProvider.class)) {
clientRole = enabledStorageProvider.getClientRole(client, name);
if (clientRole != null) return clientRole;
}
return null;
}
@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client) {
return session.roleLocalStorage().getClientRolesStream(client);
}
@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
return session.roleLocalStorage().getClientRolesStream(client, first, max);
}
/**
* Obtaining roles from an external role storage is time-bounded. In case the external role storage
* isn't available at least roles from a local storage are returned. For this purpose
* the {@link org.keycloak.services.DefaultKeycloakSessionFactory#getRoleStorageProviderTimeout()} property is used.
* Default value is 3000 milliseconds and it's configurable.
* See {@link org.keycloak.services.DefaultKeycloakSessionFactory} for details.
*/
@Override
public Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) {
Stream<RoleModel> local = session.roleLocalStorage().searchForClientRolesStream(client, search, first, max);
Stream<RoleModel> ext = getEnabledStorageProviders(session, client.getRealm(), RoleLookupProvider.class).stream()
.flatMap(ServicesUtils.timeBound(session,
roleStorageProviderTimeout,
p -> ((RoleLookupProvider) p).searchForClientRolesStream(client, search, first, max)));
return Stream.concat(local, ext);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,195 @@
/*
* Copyright 2020 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.federation;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.role.RoleStorageProvider;
import org.keycloak.storage.role.RoleStorageProviderModel;
public class HardcodedRoleStorageProvider implements RoleStorageProvider {
private final RoleStorageProviderModel component;
private final String roleName;
public HardcodedRoleStorageProvider(RoleStorageProviderModel component) {
this.component = component;
this.roleName = component.getConfig().getFirst(HardcodedRoleStorageProviderFactory.ROLE_NAME);
}
@Override
public void close() {
}
@Override
public RoleModel getRealmRole(RealmModel realm, String name) {
if (this.roleName.equals(name)) return new HardcodedRoleAdapter(realm);
return null;
}
@Override
public RoleModel getRoleById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
final String roleName = storageId.getExternalId();
if (this.roleName.equals(roleName)) return new HardcodedRoleAdapter(realm);
return null;
}
@Override
public Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) {
if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedRoleStorageProviderFactory.DELAYED_SEARCH))) try {
Thread.sleep(5000l);
} catch (InterruptedException ex) {
Logger.getLogger(HardcodedClientStorageProvider.class).warn(ex.getCause());
}
if (search != null && this.roleName.toLowerCase().contains(search.toLowerCase())) {
return Stream.of(new HardcodedRoleAdapter(realm));
}
return Stream.empty();
}
@Override
public RoleModel getClientRole(ClientModel client, String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<RoleModel> searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) {
throw new UnsupportedOperationException("Not supported yet.");
}
public class HardcodedRoleAdapter implements RoleModel {
private final RealmModel realm;
private StorageId storageId;
public HardcodedRoleAdapter(RealmModel realm) {
this.realm = realm;
}
@Override
public String getId() {
if (storageId == null) {
storageId = new StorageId(component.getId(), getName());
}
return storageId.getId();
}
@Override
public String getName() {
return roleName;
}
@Override
public String getDescription() {
return "Federated Role";
}
@Override
public boolean isComposite() {
return false;
}
@Override
public Set<RoleModel> getComposites() {
return Collections.EMPTY_SET;
}
@Override
public boolean isClientRole() {
return false;
}
@Override
public String getContainerId() {
return realm.getId();
}
@Override
public RoleContainerModel getContainer() {
return realm;
}
@Override
public boolean hasRole(RoleModel role) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String getFirstAttribute(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public List<String> getAttribute(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Map<String, List<String>> getAttributes() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setDescription(String description) {
throw new ReadOnlyException("role is read only");
}
@Override
public void setName(String name) {
throw new ReadOnlyException("role is read only");
}
@Override
public void addCompositeRole(RoleModel role) {
throw new ReadOnlyException("role is read only");
}
@Override
public void removeCompositeRole(RoleModel role) {
throw new ReadOnlyException("role is read only");
}
@Override
public void setSingleAttribute(String name, String value) {
throw new ReadOnlyException("role is read only");
}
@Override
public void setAttribute(String name, Collection<String> values) {
throw new ReadOnlyException("role is read only");
}
@Override
public void removeAttribute(String name) {
throw new ReadOnlyException("role is read only");
}
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2020 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.federation;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.role.RoleStorageProviderFactory;
import org.keycloak.storage.role.RoleStorageProviderModel;
public class HardcodedRoleStorageProviderFactory implements RoleStorageProviderFactory<HardcodedRoleStorageProvider> {
@Override
public HardcodedRoleStorageProvider create(KeycloakSession session, ComponentModel model) {
return new HardcodedRoleStorageProvider(new RoleStorageProviderModel(model));
}
public static final String PROVIDER_ID = "hardcoded-role";
public static final String ROLE_NAME = "role_name";
public static final String DELAYED_SEARCH = "delayed_search";
protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
static {
CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property().name(ROLE_NAME)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Hardcoded Role Name")
.helpText("Only this role naem is available for lookup")
.defaultValue("hardcoded-role")
.add()
.property().name(DELAYED_SEARCH)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.label("Delayes provider by 5s.")
.helpText("If true it delayes search for clients within the provider by 5s.")
.defaultValue("false")
.add()
.build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,17 @@
#
# Copyright 2020 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.federation.HardcodedRoleStorageProviderFactory

View file

@ -0,0 +1,262 @@
/*
* Copyright 2020 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.federation.storage;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
import javax.ws.rs.core.Response;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.role.RoleStorageProvider;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.federation.HardcodedRoleStorageProviderFactory;
@AuthServerContainerExclude(AuthServer.REMOTE)
public class RoleStorageTest extends AbstractTestRealmKeycloakTest {
private String providerId;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
protected String addComponent(ComponentRepresentation component) {
try (Response resp = adminClient.realm("test").components().add(component)) {
String id = ApiUtil.getCreatedId(resp);
getCleanup().addComponentId(id);
return id;
}
}
@Before
public void addProvidersBeforeTest() throws URISyntaxException, IOException {
ComponentRepresentation provider = new ComponentRepresentation();
provider.setName("role-storage-hardcoded");
provider.setProviderId(HardcodedRoleStorageProviderFactory.PROVIDER_ID);
provider.setProviderType(RoleStorageProvider.class.getName());
provider.setConfig(new MultivaluedHashMap<>());
provider.getConfig().putSingle(HardcodedRoleStorageProviderFactory.ROLE_NAME, "hardcoded-role");
provider.getConfig().putSingle(HardcodedRoleStorageProviderFactory.DELAYED_SEARCH, Boolean.toString(false));
providerId = addComponent(provider);
}
@Test
public void testGetRole() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
RoleModel hardcoded = realm.getRole("hardcoded-role");
assertNotNull(hardcoded);
});
}
@Test
public void testGetRoleById() {
String providerId = this.providerId;
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
StorageId storageId = new StorageId(providerId, "hardcoded-role");
RoleModel hardcoded = realm.getRoleById(storageId.getId());
assertNotNull(hardcoded);
});
}
@Test(timeout = 4000)
public void testSearchTimeout() {
String hardcodedRole = HardcodedRoleStorageProviderFactory.PROVIDER_ID;
String delayedSearch = HardcodedRoleStorageProviderFactory.DELAYED_SEARCH;
String providerId = this.providerId;
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName(AuthRealm.TEST);
assertThat(session.roleStorageManager()
.searchForRolesStream(realm, "role", null, null)
.map(RoleModel::getName)
.collect(Collectors.toList()),
allOf(
hasItem(hardcodedRole),
hasItem("sample-realm-role"))
);
//update the provider to simulate delay during the search
ComponentModel memoryProvider = realm.getComponent(providerId);
memoryProvider.getConfig().putSingle(delayedSearch, Boolean.toString(true));
realm.updateComponent(memoryProvider);
});
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName(AuthRealm.TEST);
// search for roles and check hardcoded-role is not present
assertThat(session.roleStorageManager()
.searchForRolesStream(realm, "role", null, null)
.map(RoleModel::getName)
.collect(Collectors.toList()),
allOf(
not(hasItem(hardcodedRole)),
hasItem("sample-realm-role")
));
});
}
/*
TODO review caching of roles, it behaves a little bit different than clients so following tests fails.
Tracked as KEYCLOAK-14938.
*/
// @Test
// public void testDailyEviction() {
// testNotCached();
//
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleStorageProviderModel model = realm.getRoleStorageProviders().get(0);
// Calendar eviction = Calendar.getInstance();
// eviction.add(Calendar.HOUR, 1);
// model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_DAILY);
// model.setEvictionHour(eviction.get(HOUR_OF_DAY));
// model.setEvictionMinute(eviction.get(MINUTE));
// realm.updateComponent(model);
// });
// testIsCached();
// setTimeOffset(2 * 60 * 60); // 2 hours in future
// testNotCached();
// testIsCached();
//
// setDefaultCachePolicy();
// testIsCached();
//
// }
//
// @Test
// public void testWeeklyEviction() {
// testNotCached();
//
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleStorageProviderModel model = realm.getRoleStorageProviders().get(0);
// Calendar eviction = Calendar.getInstance();
// eviction.add(Calendar.HOUR, 4 * 24);
// model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY);
// model.setEvictionDay(eviction.get(DAY_OF_WEEK));
// model.setEvictionHour(eviction.get(HOUR_OF_DAY));
// model.setEvictionMinute(eviction.get(MINUTE));
// realm.updateComponent(model);
// });
// testIsCached();
// setTimeOffset(2 * 24 * 60 * 60); // 2 days in future
// testIsCached();
// setTimeOffset(5 * 24 * 60 * 60); // 5 days in future
// testNotCached();
// testIsCached();
//
// setDefaultCachePolicy();
// testIsCached();
//
// }
//
// @Test
// public void testMaxLifespan() {
// testNotCached();
//
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleStorageProviderModel model = realm.getRoleStorageProviders().get(0);
// model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN);
// model.setMaxLifespan(1 * 60 * 60 * 1000);
// realm.updateComponent(model);
// });
// testIsCached();
//
// setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future
//
// testIsCached();
//
// setTimeOffset(2 * 60 * 60); // 2 hours in future
//
// testNotCached();
// testIsCached();
//
// setDefaultCachePolicy();
// testIsCached();
//
// }
//
// private void testNotCached() {
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleModel hardcoded = realm.getRole("hardcoded-role");
// Assert.assertNotNull(hardcoded);
// Assert.assertThat(hardcoded, not(instanceOf(RoleAdapter.class)));
// });
// }
//
// private void testIsCached() {
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleModel hardcoded = realm.getRole("hardcoded-role");
// Assert.assertNotNull(hardcoded);
// Assert.assertThat(hardcoded, instanceOf(RoleAdapter.class));
// });
// }
//
// @Test
// public void testNoCache() {
// testNotCached();
//
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleStorageProviderModel model = realm.getRoleStorageProviders().get(0);
// model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE);
// realm.updateComponent(model);
// });
//
// testNotCached();
//
// // test twice because updating component should evict
// testNotCached();
//
// // set it back
// setDefaultCachePolicy();
// testIsCached();
// }
//
// private void setDefaultCachePolicy() {
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// RoleStorageProviderModel model = realm.getRoleStorageProviders().get(0);
// model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.DEFAULT);
// realm.updateComponent(model);
// });
// }
}