KEYCLOAK-14973 Create GroupStorageManager

This commit is contained in:
mhajas 2020-08-11 14:27:39 +02:00 committed by Hynek Mlnařík
parent 03c07bd2d7
commit bdccfef513
18 changed files with 1168 additions and 38 deletions

View file

@ -167,7 +167,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public GroupProvider getGroupDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (groupDelegate != null) return groupDelegate;
groupDelegate = session.groupLocalStorage();
groupDelegate = session.groupStorageManager();
return groupDelegate;
}

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.group;
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 GroupStorageProviderFactory<T extends GroupStorageProvider> extends ComponentFactory<T, GroupStorageProvider> {
/**
* 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 GroupStorageProviderModel 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 GroupStorageProvider implementations
*
* @return
*/
@Override
default
List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return GroupStorageProviderSpi.commonConfig();
}
@Override
default
Map<String, Object> getTypeMetadata() {
return new HashMap<>();
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.group;
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 org.keycloak.storage.role.RoleStorageProvider;
import org.keycloak.storage.role.RoleStorageProviderFactory;
import java.util.Collections;
import java.util.List;
public class GroupStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "group-storage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return GroupStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return GroupStorageProviderFactory.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

@ -76,6 +76,7 @@ org.keycloak.keys.PublicKeyStorageSpi
org.keycloak.keys.KeySpi
org.keycloak.storage.client.ClientStorageProviderSpi
org.keycloak.storage.role.RoleStorageProviderSpi
org.keycloak.storage.group.GroupStorageProviderSpi
org.keycloak.crypto.SignatureSpi
org.keycloak.crypto.ClientSignatureVerifierSpi
org.keycloak.crypto.HashSpi

View file

@ -18,6 +18,7 @@
package org.keycloak.models;
import org.keycloak.provider.Provider;
import org.keycloak.storage.group.GroupLookupProvider;
import java.util.List;
import java.util.stream.Collectors;
@ -29,7 +30,7 @@ import java.util.stream.Stream;
* @author mhajas
*
*/
public interface GroupProvider extends Provider {
public interface GroupProvider extends Provider, GroupLookupProvider {
/**
* Returns a group from the given realm with the corresponding id
@ -43,15 +44,6 @@ public interface GroupProvider extends Provider {
return getGroupById(realm, id);
}
/**
* Returns a group from the given realm with the corresponding id
*
* @param realm Realm.
* @param id Id.
* @return GroupModel with the corresponding id.
*/
GroupModel getGroupById(RealmModel realm, String id);
/**
* Returns groups for the given realm.
*
@ -160,32 +152,6 @@ public interface GroupProvider extends Provider {
*/
Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer firstResult, Integer maxResults);
/**
* Returns groups with the given string in name for the given realm.
*
* @param realm Realm.
* @param search Searched string.
* @param firstResult First result to return. Ignored if {@code null}.
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
* @return List of groups with the given string in name.
* @deprecated Use {@link #searchForGroupByNameStream(RealmModel, String, Integer, Integer)} searchForGroupByNameStream} instead.
*/
@Deprecated
default List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
return searchForGroupByNameStream(realm, search, firstResult, maxResults).collect(Collectors.toList());
}
/**
* Returns groups with the given string in name for the given realm.
*
* @param realm Realm.
* @param search Searched string.
* @param firstResult First result to return. Ignored if {@code null}.
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
* @return Stream of groups with the given string in name.
*/
Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
/**
* Creates a new group with the given name in the given realm.
* Effectively the same as {@code createGroup(realm, null, name, null)}.

View file

@ -170,6 +170,11 @@ public interface KeycloakSession {
*/
RoleProvider roleStorageManager();
/**
* @return GroupStorageManager instance
*/
GroupProvider groupStorageManager();
/**
* Un-cached view of all users in system including users loaded by UserStorageProviders
*

View file

@ -19,6 +19,7 @@ package org.keycloak.models;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.component.ComponentModel;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
@ -423,6 +424,16 @@ public interface RealmModel extends RoleContainerModel {
void removeComponents(String parentId);
List<ComponentModel> getComponents(String parentId, String providerType);
/**
* Returns stream of ComponentModels for specific parentId and providerType.
* @param parentId id of parent
* @param providerType type of provider
* @return stream of ComponentModels
*/
default Stream<ComponentModel> getComponentsStream(String parentId, String providerType) {
return getComponents(parentId, providerType).stream();
}
List<ComponentModel> getComponents(String parentId);
List<ComponentModel> getComponents();
@ -458,6 +469,15 @@ public interface RealmModel extends RoleContainerModel {
return list;
}
/**
* Returns stream of ComponentModels that represent StorageProviders for class storageProviderClass in this realm
* @param storageProviderClass class
* @return stream of StorageProviders
*/
default Stream<ComponentModel> getStorageProviders(Class<? extends Provider> storageProviderClass) {
return getComponentsStream(getId(), storageProviderClass.getName());
}
String getLoginTheme();
void setLoginTheme(String name);

View file

@ -0,0 +1,64 @@
/*
* 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.group;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public interface GroupLookupProvider {
/**
* Returns a group from the given realm with the corresponding id
*
* @param realm Realm.
* @param id Id.
* @return GroupModel with the corresponding id.
*/
GroupModel getGroupById(RealmModel realm, String id);
/**
* Returns groups with the given string in name for the given realm.
*
* @param realm Realm.
* @param search Searched string.
* @param firstResult First result to return. Ignored if {@code null}.
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
* @return List of groups with the given string in name.
* @deprecated Use {@link #searchForGroupByNameStream(RealmModel, String, Integer, Integer)} searchForGroupByNameStream} instead.
*/
@Deprecated
default List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
return searchForGroupByNameStream(realm, search, firstResult, maxResults).collect(Collectors.toList());
}
/**
* Returns groups with the given string in name for the given realm.
*
* @param realm Realm.
* @param search Searched string.
* @param firstResult First result to return. Ignored if {@code null}.
* @param maxResults Maximum number of results to return. Ignored if {@code null}.
* @return Stream of groups with the given string in name.
*/
Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
}

View file

@ -0,0 +1,22 @@
/*
* 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.group;
import org.keycloak.provider.Provider;
public interface GroupStorageProvider extends Provider, GroupLookupProvider {
}

View file

@ -0,0 +1,56 @@
/*
* 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.group;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.CacheableStorageProviderModel;
/**
* Stored configuration of a Group Storage provider instance.
*/
public class GroupStorageProviderModel extends CacheableStorageProviderModel {
public GroupStorageProviderModel() {
setProviderType(GroupStorageProvider.class.getName());
}
public GroupStorageProviderModel(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

@ -43,6 +43,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.GroupStorageManager;
import org.keycloak.storage.RoleStorageManager;
import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
@ -75,6 +76,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
private UserStorageManager userStorageManager;
private ClientStorageManager clientStorageManager;
private RoleStorageManager roleStorageManager;
private GroupStorageManager groupStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
private UserSessionProvider sessionProvider;
private AuthenticationSessionProvider authenticationSessionProvider;
@ -122,7 +124,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
if (cache != null) {
return cache;
} else {
return groupLocalStorage();
return groupStorageManager();
}
}
@ -228,6 +230,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
return roleStorageManager;
}
@Override
public GroupProvider groupStorageManager() {
if (groupStorageManager == null) {
groupStorageManager = new GroupStorageManager(this);
}
return groupStorageManager;
}
@Override
public UserProvider userStorageManager() {

View file

@ -0,0 +1,152 @@
/*
* 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 org.keycloak.Config;
import org.keycloak.component.ComponentFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.keycloak.utils.ServicesUtils;
import java.util.function.Function;
import java.util.stream.Stream;
/**
*
* @param <ProviderType> This type will be used for looking for factories that produce instances of desired providers
* @param <StorageProviderModelType> Type of model used for creating provider, it needs to extend
* CacheableStorageProviderModel as it has {@code isEnabled()} method and also extend
* PrioritizedComponentModel which is required for sorting providers based on its
* priorities
*/
public abstract class AbstractStorageManager<ProviderType extends Provider,
StorageProviderModelType extends CacheableStorageProviderModel> {
/**
* 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 static final Long STORAGE_PROVIDER_DEFAULT_TIMEOUT = 3000L;
protected final KeycloakSession session;
private final Class<ProviderType> providerTypeClass;
private final Function<ComponentModel, StorageProviderModelType> toStorageProviderModelTypeFunction;
private final String configScope;
private Long storageProviderTimeout;
public AbstractStorageManager(KeycloakSession session, Class<ProviderType> providerTypeClass, Function<ComponentModel, StorageProviderModelType> toStorageProviderModelTypeFunction, String configScope) {
this.session = session;
this.providerTypeClass = providerTypeClass;
this.toStorageProviderModelTypeFunction = toStorageProviderModelTypeFunction;
this.configScope = configScope;
}
protected Long getStorageProviderTimeout() {
if (storageProviderTimeout == null) {
storageProviderTimeout = Config.scope(configScope).getLong("storageProviderTimeout", STORAGE_PROVIDER_DEFAULT_TIMEOUT);
}
return storageProviderTimeout;
}
/**
* Returns a factory with the providerId, which produce instances of type CreatedProviderType
* @param providerId id of factory that produce desired instances
* @return A factory that implements {@code ComponentFactory<CreatedProviderType, ProviderType>}
*/
protected <T extends ProviderType> ComponentFactory<T, ProviderType> getStorageProviderFactory(String providerId) {
return (ComponentFactory<T, ProviderType>) session.getKeycloakSessionFactory()
.getProviderFactory(providerTypeClass, providerId);
}
/**
*
* @param realm realm
* @return enabled storage providers for realm and @{code getProviderTypeClass()}
*/
protected Stream<ProviderType> getEnabledStorageProviders(RealmModel realm) {
return getStorageProviderModels(realm, providerTypeClass)
.map(toStorageProviderModelTypeFunction)
.filter(StorageProviderModelType::isEnabled)
.sorted(StorageProviderModelType.comparator)
.map(this::getStorageProviderInstance);
}
/**
* Gets all enabled StorageProviders, applies applyFunction on each of them and then join the results together.
*
* !! Each StorageProvider has a limited time to respond, if it fails to do it, empty stream is returned !!
*
* @param realm realm
* @param applyFunction function that is applied on StorageProviders
* @param <R> result of applyFunction
* @return a stream with all results from all StorageProviders
*/
protected <R> Stream<R> applyOnEnabledStorageProvidersWithTimeout(RealmModel realm, Function<ProviderType, ? extends Stream<R>> applyFunction) {
return getEnabledStorageProviders(realm).flatMap(ServicesUtils.timeBound(session,
getStorageProviderTimeout(), applyFunction));
}
/**
* Returns an instance of provider with the providerId within the realm.
* @param realm realm
* @param providerId id of ComponentModel within database/storage
* @return an instance of type CreatedProviderType
*/
protected ProviderType getStorageProviderInstance(RealmModel realm, String providerId) {
ComponentModel componentModel = realm.getComponent(providerId);
if (componentModel == null) {
return null;
}
return getStorageProviderInstance(toStorageProviderModelTypeFunction.apply(componentModel));
}
/**
* Returns an instance of provider for the model
* @param model StorageProviderModel obtained from database/storage
* @return an instance of type CreatedProviderType
*/
protected ProviderType getStorageProviderInstance(StorageProviderModelType model) {
if (model == null || !model.isEnabled()) {
return null;
}
@SuppressWarnings("unchecked")
ProviderType instance = (ProviderType) session.getAttribute(model.getId());
if (instance != null) return instance;
ComponentFactory<? extends ProviderType, ProviderType> factory = getStorageProviderFactory(model.getProviderId());
instance = factory.create(session, model);
if (instance == null) {
throw new IllegalStateException("StorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance");
}
session.enlistForClose(instance);
session.setAttribute(model.getId(), instance);
return instance;
}
/**
* Stream of ComponentModels of storageType.
* @param realm Realm.
* @param storageType Type.
* @return Stream of ComponentModels
*/
public static Stream<ComponentModel> getStorageProviderModels(RealmModel realm, Class<? extends Provider> storageType) {
return realm.getStorageProviders(storageType);
}
}

View file

@ -0,0 +1,124 @@
/*
* 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 org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.storage.group.GroupLookupProvider;
import org.keycloak.storage.group.GroupStorageProvider;
import org.keycloak.storage.group.GroupStorageProviderModel;
import java.util.stream.Stream;
public class GroupStorageManager extends AbstractStorageManager<GroupStorageProvider, GroupStorageProviderModel> implements GroupProvider {
public GroupStorageManager(KeycloakSession session) {
super(session, GroupStorageProvider.class, GroupStorageProviderModel::new, "group");
}
/* GROUP PROVIDER LOOKUP METHODS - implemented by group storage providers */
@Override
public GroupModel getGroupById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
if (storageId.getProviderId() == null) {
return session.groupLocalStorage().getGroupById(realm, id);
}
GroupLookupProvider provider = getStorageProviderInstance(realm, storageId.getProviderId());
if (provider == null) return null;
return provider.getGroupById(realm, id);
}
/**
* Obtaining groups from an external client storage is time-bounded. In case the external group storage
* isn't available at least groups from a local storage are returned. For this purpose
* the {@link org.keycloak.services.DefaultKeycloakSessionFactory#getClientStorageProviderTimeout()} property is used.
* Default value is 3000 milliseconds and it's configurable.
* See {@link org.keycloak.services.DefaultKeycloakSessionFactory} for details.
*
*/
@Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = session.groupLocalStorage().searchForGroupByNameStream(realm, search, firstResult, maxResults);
Stream<GroupModel> ext = applyOnEnabledStorageProvidersWithTimeout(realm,
p -> ((GroupLookupProvider) p).searchForGroupByNameStream(realm, search, firstResult, maxResults));
return Stream.concat(local, ext);
}
/* GROUP PROVIDER METHODS - provided only by local storage (e.g. not supported by storage providers) */
@Override
public Stream<GroupModel> getGroupsStream(RealmModel realm) {
return session.groupLocalStorage().getGroupsStream(realm);
}
@Override
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
return session.groupLocalStorage().getGroupsCount(realm, onlyTopGroups);
}
@Override
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
return session.groupLocalStorage().getGroupsCountByNameContaining(realm, search);
}
@Override
public Stream<GroupModel> getGroupsByRoleStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
return session.groupLocalStorage().getGroupsByRoleStream(realm, role, firstResult, maxResults);
}
@Override
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm) {
return session.groupLocalStorage().getTopLevelGroupsStream(realm);
}
@Override
public Stream<GroupModel> getTopLevelGroupsStream(RealmModel realm, Integer firstResult, Integer maxResults) {
return session.groupLocalStorage().getTopLevelGroupsStream(realm, firstResult, maxResults);
}
@Override
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
return session.groupLocalStorage().createGroup(realm, id, name, toParent);
}
@Override
public boolean removeGroup(RealmModel realm, GroupModel group) {
return session.groupLocalStorage().removeGroup(realm, group);
}
@Override
public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) {
session.groupLocalStorage().moveGroup(realm, group, toParent);
}
@Override
public void addTopLevelGroup(RealmModel realm, GroupModel subGroup) {
session.groupLocalStorage().addTopLevelGroup(realm, subGroup);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,192 @@
/*
* 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 org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.group.GroupStorageProvider;
import org.keycloak.storage.group.GroupStorageProviderModel;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class HardcodedGroupStorageProvider implements GroupStorageProvider {
private final GroupStorageProviderModel component;
private final String groupName;
public HardcodedGroupStorageProvider(GroupStorageProviderModel component) {
this.component = component;
this.groupName = component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.GROUP_NAME);
}
@Override
public void close() {
}
@Override
public GroupModel getGroupById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
final String groupName = storageId.getExternalId();
if (this.groupName.equals(groupName)) return new HardcodedGroupAdapter(realm);
return null;
}
@Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH))) try {
Thread.sleep(5000l);
} catch (InterruptedException ex) {
Logger.getLogger(HardcodedGroupStorageProvider.class).warn(ex.getCause());
}
if (search != null && this.groupName.toLowerCase().contains(search.toLowerCase())) {
return Stream.of(new HardcodedGroupAdapter(realm));
}
return Stream.empty();
}
public class HardcodedGroupAdapter implements GroupModel {
private final RealmModel realm;
private StorageId storageId;
public HardcodedGroupAdapter(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 groupName;
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
return null;
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
return null;
}
@Override
public boolean hasRole(RoleModel role) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Set<RoleModel> getRoleMappings() {
return null;
}
@Override
public String getFirstAttribute(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Stream<String> getAttributeStream(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Map<String, List<String>> getAttributes() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public GroupModel getParent() {
return null;
}
@Override
public String getParentId() {
return null;
}
@Override
public Stream<GroupModel> getSubGroupsStream() {
return Stream.empty();
}
@Override
public void deleteRoleMapping(RoleModel role) {
throw new ReadOnlyException("group is read only");
}
@Override
public void grantRole(RoleModel role) {
throw new ReadOnlyException("group is read only");
}
@Override
public void setParent(GroupModel group) {
throw new ReadOnlyException("group is read only");
}
@Override
public void addChild(GroupModel subGroup) {
throw new ReadOnlyException("group is read only");
}
@Override
public void removeChild(GroupModel subGroup) {
throw new ReadOnlyException("group is read only");
}
@Override
public void setName(String name) {
throw new ReadOnlyException("group is read only");
}
@Override
public void setSingleAttribute(String name, String value) {
throw new ReadOnlyException("group is read only");
}
@Override
public void setAttribute(String name, List<String> values) {
throw new ReadOnlyException("group is read only");
}
@Override
public void removeAttribute(String name) {
throw new ReadOnlyException("group is read only");
}
}
}

View file

@ -0,0 +1,66 @@
/*
* 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 org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.group.GroupStorageProviderFactory;
import org.keycloak.storage.group.GroupStorageProviderModel;
import java.util.List;
public class HardcodedGroupStorageProviderFactory implements GroupStorageProviderFactory<HardcodedGroupStorageProvider> {
@Override
public HardcodedGroupStorageProvider create(KeycloakSession session, ComponentModel model) {
return new HardcodedGroupStorageProvider(new GroupStorageProviderModel(model));
}
public static final String PROVIDER_ID = "hardcoded-group";
public static final String GROUP_NAME = "gorup_name";
public static final String DELAYED_SEARCH = "delayed_search";
protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
static {
CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property().name(GROUP_NAME)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Hardcoded Group Name")
.helpText("Only this group name is available for lookup")
.defaultValue("hardcoded-group")
.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,257 @@
/*
* 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 org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.group.GroupStorageProvider;
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.HardcodedGroupStorageProviderFactory;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
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;
@AuthServerContainerExclude(AuthServer.REMOTE)
public class GroupStorageTest 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("group-storage-hardcoded");
provider.setProviderId(HardcodedGroupStorageProviderFactory.PROVIDER_ID);
provider.setProviderType(GroupStorageProvider.class.getName());
provider.setConfig(new MultivaluedHashMap<>());
provider.getConfig().putSingle(HardcodedGroupStorageProviderFactory.GROUP_NAME, "hardcoded-group");
provider.getConfig().putSingle(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH, Boolean.toString(false));
providerId = addComponent(provider);
}
@Test
public void testGetGroupById() {
String providerId = this.providerId;
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
StorageId storageId = new StorageId(providerId, "hardcoded-group");
GroupModel hardcoded = session.groups().getGroupById(realm, storageId.getId());
assertNotNull(hardcoded);
});
}
@Test(timeout = 4000)
public void testSearchTimeout() {
String hardcodedGroup = HardcodedGroupStorageProviderFactory.PROVIDER_ID;
String delayedSearch = HardcodedGroupStorageProviderFactory.DELAYED_SEARCH;
String providerId = this.providerId;
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName(AuthRealm.TEST);
assertThat(session.groupStorageManager()
.searchForGroupByName(realm, "group", null, null).stream()
.map(GroupModel::getName)
.collect(Collectors.toList()),
allOf(
hasItem(hardcodedGroup),
hasItem("sample-realm-group"))
);
//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 groups and check hardcoded-group is not present
assertThat(session.groupStorageManager()
.searchForGroupByName(realm, "group", null, null).stream()
.map(GroupModel::getName)
.collect(Collectors.toList()),
allOf(
not(hasItem(hardcodedGroup)),
hasItem("sample-realm-group")
));
});
}
/*
TODO review caching of groups, it behaves a little bit different than clients so following tests fails.
Tracked as KEYCLOAK-15135.
*/
// @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() {
// String providerId = this.providerId;
// testingClient.server().run(session -> {
// RealmModel realm = session.realms().getRealmByName("test");
// StorageId storageId = new StorageId(providerId, "hardcoded-group");
// GroupModel hardcoded = session.groups().getGroupById(realm, storageId.getId());
// Assert.assertNotNull(hardcoded);
// Assert.assertThat(hardcoded, not(instanceOf(GroupAdapter.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);
// });
// }
}

View file

@ -575,6 +575,9 @@
}
}
]
},
{
"name": "sample-realm-group"
}
],