From 6b00633c47cba2b7bb823b01226e54f3cf28b2db Mon Sep 17 00:00:00 2001 From: vramik Date: Tue, 21 Jul 2020 12:34:50 +0200 Subject: [PATCH] KEYCLOAK-14812 Create RoleStorageManager --- .../cache/infinispan/RealmCacheSession.java | 3 +- .../role/RoleStorageProviderFactory.java | 109 ++++++++ .../storage/role/RoleStorageProviderSpi.java | 80 ++++++ .../services/org.keycloak.provider.Spi | 1 + .../org/keycloak/models/KeycloakSession.java | 7 +- .../java/org/keycloak/models/RealmModel.java | 12 + .../org/keycloak/models/RoleProvider.java | 50 +--- .../storage/role/RoleLookupProvider.java | 74 +++++ .../storage/role/RoleStorageProvider.java | 27 ++ .../role/RoleStorageProviderModel.java | 57 ++++ .../services/DefaultKeycloakSession.java | 19 +- .../DefaultKeycloakSessionFactory.java | 18 +- .../keycloak/storage/RoleStorageManager.java | 249 +++++++++++++++++ .../HardcodedRoleStorageProvider.java | 195 +++++++++++++ .../HardcodedRoleStorageProviderFactory.java | 65 +++++ ...ak.storage.role.RoleStorageProviderFactory | 17 ++ .../federation/storage/RoleStorageTest.java | 262 ++++++++++++++++++ 17 files changed, 1180 insertions(+), 65 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderSpi.java create mode 100644 server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java create mode 100644 server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProvider.java create mode 100644 server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProviderModel.java create mode 100644 services/src/main/java/org/keycloak/storage/RoleStorageManager.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProviderFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.role.RoleStorageProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/RoleStorageTest.java diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 087c2b8f1d..d0eb08faea 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -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; } diff --git a/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderFactory.java new file mode 100644 index 0000000000..ca88dbd741 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderFactory.java @@ -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 extends ComponentFactory { + + + /** + * 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 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 getCommonProviderConfigProperties() { + return RoleStorageProviderSpi.commonConfig(); + } + + @Override + default + Map getTypeMetadata() { + return new HashMap<>(); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderSpi.java b/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderSpi.java new file mode 100644 index 0000000000..a2fdc79e37 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/role/RoleStorageProviderSpi.java @@ -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 getProviderClass() { + return RoleStorageProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return RoleStorageProviderFactory.class; + } + + private static final List commonConfig; + + static { + //corresponds to properties defined in CacheableStorageProviderModel and PrioritizedComponentModel + List 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 commonConfig() { + return commonConfig; + } + +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 640bd645ae..e9914bc732 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -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 diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index 9ede32cee6..d8d28d67d0 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -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 */ diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 5f8baf2298..a799e3c322 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -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 getRoleStorageProviders() { + List 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); diff --git a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java index 6ee50ef9f5..7b6dde6f56 100644 --- a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java @@ -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 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 searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max); } diff --git a/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java new file mode 100644 index 0000000000..400d1e03b3 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/role/RoleLookupProvider.java @@ -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 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 searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max); +} diff --git a/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProvider.java new file mode 100644 index 0000000000..3521561cb7 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProvider.java @@ -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 { + +} + diff --git a/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProviderModel.java new file mode 100644 index 0000000000..2eb928babb --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/role/RoleStorageProviderModel.java @@ -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; + + } +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 9aebe2480a..2dfb830604 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -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 diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index 1fe799cd8f..71d8730812 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -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 */ diff --git a/services/src/main/java/org/keycloak/storage/RoleStorageManager.java b/services/src/main/java/org/keycloak/storage/RoleStorageManager.java new file mode 100644 index 0000000000..e75884b5b5 --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/RoleStorageManager.java @@ -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 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 List getStorageProviders(KeycloakSession session, RealmModel realm, Class type) { + List 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 List getEnabledStorageProviders(KeycloakSession session, RealmModel realm, Class type) { + List 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 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 searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) { + Stream local = session.roleLocalStorage().searchForRolesStream(realm, search, first, max); + Stream 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 getClientRolesStream(ClientModel client) { + return session.roleLocalStorage().getClientRolesStream(client); + } + + @Override + public Stream 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 searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) { + Stream local = session.roleLocalStorage().searchForClientRolesStream(client, search, first, max); + Stream 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() { + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java new file mode 100644 index 0000000000..685e205c5d --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java @@ -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 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 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 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 getAttribute(String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Map> 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 values) { + throw new ReadOnlyException("role is read only"); + } + + @Override + public void removeAttribute(String name) { + throw new ReadOnlyException("role is read only"); + } + } + + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProviderFactory.java new file mode 100644 index 0000000000..59f3615e80 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProviderFactory.java @@ -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 { + @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 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 getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.role.RoleStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.role.RoleStorageProviderFactory new file mode 100644 index 0000000000..f5061ba38d --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.role.RoleStorageProviderFactory @@ -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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/RoleStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/RoleStorageTest.java new file mode 100644 index 0000000000..a990b744c1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/RoleStorageTest.java @@ -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); +// }); +// } +}