Map Store Removal: Rename legacy modules

Closes #24107

Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Martin Kanis
2024-01-24 15:39:29 +01:00
committed by Alexander Schwartz
parent 28c9f98930
commit 7797f778d1
175 changed files with 45 additions and 37 deletions
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-model-pom</artifactId>
<groupId>org.keycloak</groupId>
<version>999.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-model-storage</artifactId>
<name>Keycloak Database Support</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,281 @@
/*
* Copyright 2022. Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.credential;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SubjectCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.storage.AbstractStorageManager;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.StoreManagers;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Handling credentials for a given user for the store.
*
* @author Alexander Schwartz
*/
public class UserCredentialManager extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel> implements SubjectCredentialManager {
private final UserModel user;
private final KeycloakSession session;
private final RealmModel realm;
public UserCredentialManager(KeycloakSession session, RealmModel realm, UserModel user) {
super(session, UserStorageProviderFactory.class, UserStorageProvider.class, UserStorageProviderModel::new, "user");
this.user = user;
this.session = session;
this.realm = realm;
}
@Override
public boolean isValid(List<CredentialInput> inputs) {
if (!isValid(user)) {
return false;
}
List<CredentialInput> toValidate = new LinkedList<>(inputs);
String providerId = StorageId.isLocalStorage(user.getId()) ? user.getFederationLink() : StorageId.providerId(user.getId());
if (providerId != null) {
UserStorageProviderModel model = getStorageProviderModel(realm, providerId);
if (model == null || !model.isEnabled()) return false;
CredentialInputValidator validator = getStorageProviderInstance(model, CredentialInputValidator.class);
if (validator != null) {
validate(realm, user, toValidate, validator);
}
}
getCredentialProviders(session, CredentialInputValidator.class)
.forEach(validator -> validate(realm, user, toValidate, validator));
return toValidate.isEmpty();
}
@Override
public boolean updateCredential(CredentialInput input) {
String providerId = StorageId.isLocalStorage(user.getId()) ? user.getFederationLink() : StorageId.providerId(user.getId());
if (!StorageId.isLocalStorage(user.getId())) throwExceptionIfInvalidUser(user);
if (providerId != null) {
UserStorageProviderModel model = getStorageProviderModel(realm, providerId);
if (model == null || !model.isEnabled()) return false;
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
if (updater != null && updater.supportsCredentialType(input.getType())) {
if (updater.updateCredential(realm, user, input)) return true;
}
}
return getCredentialProviders(session, CredentialInputUpdater.class)
.filter(updater -> updater.supportsCredentialType(input.getType()))
.anyMatch(updater -> updater.updateCredential(realm, user, input));
}
@Override
public void updateStoredCredential(CredentialModel cred) {
throwExceptionIfInvalidUser(user);
getStoreForUser(user).updateCredential(realm, user, cred);
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
throwExceptionIfInvalidUser(user);
return getStoreForUser(user).createCredential(realm, user, cred);
}
@Override
public boolean removeStoredCredentialById(String id) {
throwExceptionIfInvalidUser(user);
return getStoreForUser(user).removeStoredCredential(realm, user, id);
}
@Override
public CredentialModel getStoredCredentialById(String id) {
return getStoreForUser(user).getStoredCredentialById(realm, user, id);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
return getStoreForUser(user).getStoredCredentialsStream(realm, user);
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return getStoreForUser(user).getStoredCredentialsByTypeStream(realm, user, type);
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
return getStoreForUser(user).getStoredCredentialByNameAndType(realm, user, name, type);
}
@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
throwExceptionIfInvalidUser(user);
return getStoreForUser(user).moveCredentialTo(realm, user, id, newPreviousCredentialId);
}
@Override
public void updateCredentialLabel(String credentialId, String userLabel) {
throwExceptionIfInvalidUser(user);
CredentialModel credential = getStoredCredentialById(credentialId);
credential.setUserLabel(userLabel);
updateStoredCredential(credential);
}
@Override
public void disableCredentialType(String credentialType) {
String providerId = StorageId.isLocalStorage(user.getId()) ? user.getFederationLink() : StorageId.providerId(user.getId());
if (!StorageId.isLocalStorage(user.getId())) throwExceptionIfInvalidUser(user);
if (providerId != null) {
UserStorageProviderModel model = getStorageProviderModel(realm, providerId);
if (model == null || !model.isEnabled()) return;
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
if (updater.supportsCredentialType(credentialType)) {
updater.disableCredentialType(realm, user, credentialType);
}
}
getCredentialProviders(session, CredentialInputUpdater.class)
.filter(updater -> updater.supportsCredentialType(credentialType))
.forEach(updater -> updater.disableCredentialType(realm, user, credentialType));
}
@Override
public Stream<String> getDisableableCredentialTypesStream() {
Stream<String> types = Stream.empty();
String providerId = StorageId.isLocalStorage(user) ? user.getFederationLink() : StorageId.resolveProviderId(user);
if (providerId != null) {
UserStorageProviderModel model = getStorageProviderModel(realm, providerId);
if (model == null || !model.isEnabled()) return types;
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
if (updater != null) types = updater.getDisableableCredentialTypesStream(realm, user);
}
return Stream.concat(types, getCredentialProviders(session, CredentialInputUpdater.class)
.flatMap(updater -> updater.getDisableableCredentialTypesStream(realm, user)))
.distinct();
}
@Override
public boolean isConfiguredFor(String type) {
UserStorageCredentialConfigured userStorageConfigured = isConfiguredThroughUserStorage(realm, user, type);
// Check if we can rely just on userStorage to decide if credential is configured for the user or not
switch (userStorageConfigured) {
case CONFIGURED: return true;
case USER_STORAGE_DISABLED: return false;
}
// Check locally as a fallback
return isConfiguredLocally(type);
}
@Override
public boolean isConfiguredLocally(String type) {
return getCredentialProviders(session, CredentialInputValidator.class)
.anyMatch(validator -> validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type));
}
@Override
public Stream<String> getConfiguredUserStorageCredentialTypesStream() {
return getCredentialProviders(session, CredentialProvider.class).map(CredentialProvider::getType)
.filter(credentialType -> UserStorageCredentialConfigured.CONFIGURED == isConfiguredThroughUserStorage(realm, user, credentialType));
}
@Override
public CredentialModel createCredentialThroughProvider(CredentialModel model) {
throwExceptionIfInvalidUser(user);
return session.getKeycloakSessionFactory()
.getProviderFactoriesStream(CredentialProvider.class)
.map(f -> session.getProvider(CredentialProvider.class, f.getId()))
.filter(provider -> Objects.equals(provider.getType(), model.getType()))
.map(cp -> cp.createCredential(realm, user, cp.getCredentialFromModel(model)))
.findFirst()
.orElse(null);
}
private enum UserStorageCredentialConfigured {
CONFIGURED,
USER_STORAGE_DISABLED,
NOT_CONFIGURED
}
private UserStorageCredentialConfigured isConfiguredThroughUserStorage(RealmModel realm, UserModel user, String type) {
String providerId = StorageId.isLocalStorage(user) ? user.getFederationLink() : StorageId.resolveProviderId(user);
if (providerId != null) {
UserStorageProviderModel model = getStorageProviderModel(realm, providerId);
if (model == null || !model.isEnabled()) return UserStorageCredentialConfigured.USER_STORAGE_DISABLED;
CredentialInputValidator validator = getStorageProviderInstance(model, CredentialInputValidator.class);
if (validator != null && validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) {
return UserStorageCredentialConfigured.CONFIGURED;
}
}
return UserStorageCredentialConfigured.NOT_CONFIGURED;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isValid(UserModel user) {
Objects.requireNonNull(user);
return user.getServiceAccountClientLink() == null;
}
private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate, CredentialInputValidator validator) {
toValidate.removeIf(input -> validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input));
}
private static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
//noinspection unchecked
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
.map(f -> (T) session.getProvider(CredentialProvider.class, f.getId()));
}
private void throwExceptionIfInvalidUser(UserModel user) {
if (!isValid(user)) {
throw new RuntimeException("You can not manage credentials for this user");
}
}
private UserCredentialStore getStoreForUser(UserModel user) {
StoreManagers p = (StoreManagers) session.getProvider(DatastoreProvider.class);
if (StorageId.isLocalStorage(user.getId())) {
return (UserCredentialStore) p.userLocalStorage();
} else {
return (UserCredentialStore) p.userFederatedStorage();
}
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
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.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Alexander Schwartz
*/
public interface StorageProviderRealmModel extends RealmModel {
/**
* @deprecated Use {@link #getClientStorageProvidersStream() getClientStorageProvidersStream} instead.
*/
@Deprecated
default List<ClientStorageProviderModel> getClientStorageProviders() {
return getClientStorageProvidersStream().collect(Collectors.toList());
}
/**
* Returns sorted {@link ClientStorageProviderModel ClientStorageProviderModel} as a stream.
* It should be used with forEachOrdered if the ordering is required.
* @return Sorted stream of {@link ClientStorageProviderModel}. Never returns {@code null}.
*/
default Stream<ClientStorageProviderModel> getClientStorageProvidersStream() {
return getComponentsStream(getId(), ClientStorageProvider.class.getName())
.map(ClientStorageProviderModel::new)
.sorted(ClientStorageProviderModel.comparator);
}
/**
* @deprecated Use {@link #getRoleStorageProvidersStream() getRoleStorageProvidersStream} instead.
*/
@Deprecated
default List<RoleStorageProviderModel> getRoleStorageProviders() {
return getRoleStorageProvidersStream().collect(Collectors.toList());
}
/**
* Returns sorted {@link RoleStorageProviderModel RoleStorageProviderModel} as a stream.
* It should be used with forEachOrdered if the ordering is required.
* @return Sorted stream of {@link RoleStorageProviderModel}. Never returns {@code null}.
*/
default Stream<RoleStorageProviderModel> getRoleStorageProvidersStream() {
return getComponentsStream(getId(), RoleStorageProvider.class.getName())
.map(RoleStorageProviderModel::new)
.sorted(RoleStorageProviderModel.comparator);
}
/**
* @deprecated Use {@link #getUserStorageProvidersStream() getUserStorageProvidersStream} instead.
*/
@Deprecated
default List<UserStorageProviderModel> getUserStorageProviders() {
return getUserStorageProvidersStream().collect(Collectors.toList());
}
/**
* Returns sorted {@link UserStorageProviderModel UserStorageProviderModel} as a stream.
* It should be used with forEachOrdered if the ordering is required.
* @return Sorted stream of {@link UserStorageProviderModel}. Never returns {@code null}.
*/
default Stream<UserStorageProviderModel> getUserStorageProvidersStream() {
return getComponentsStream(getId(), UserStorageProvider.class.getName())
.map(UserStorageProviderModel::new)
.sorted(UserStorageProviderModel.comparator);
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CachedObject {
long getCacheTimestamp();
}
@@ -0,0 +1,59 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache;
import org.keycloak.models.UserModel;
import java.util.concurrent.ConcurrentMap;
/**
* Cached users will implement this interface
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CachedUserModel extends UserModel {
/**
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
*
* @return
*/
UserModel getDelegateForUpdate();
boolean isMarkedForEviction();
/**
* Invalidate the cache for this model
*
*/
void invalidate();
/**
* When was the model was loaded from database.
*
* @return
*/
long getCacheTimestamp();
/**
* Returns a map that contains custom things that are cached along with this model. You can write to this map.
*
* @return
*/
ConcurrentMap getCachedWith();
}
@@ -0,0 +1,28 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface OnUserCache {
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
/**
* All these methods effect an entire cluster of Keycloak instances.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserCache extends UserProvider {
/**
* Evict user from cache.
*
* @param user
*/
void evict(RealmModel realm, UserModel user);
/**
* Evict users of a specific realm
*
* @param realm
*/
void evict(RealmModel realm);
/**
* Clear cache entirely.
*
*/
void clear();
}
@@ -0,0 +1,247 @@
/*
* 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.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.reflections.Types;
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.provider.ProviderFactory;
import org.keycloak.utils.ServicesUtils;
import java.util.Objects;
import java.util.function.Consumer;
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> {
private static final Logger LOG = Logger.getLogger(AbstractStorageManager.class);
/**
* 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 Class<? extends ProviderFactory> factoryTypeClass;
private final Function<ComponentModel, StorageProviderModelType> toStorageProviderModelTypeFunction;
private final String configScope;
private Long storageProviderTimeout;
public AbstractStorageManager(KeycloakSession session, Class<? extends ProviderFactory> factoryTypeClass, Class<ProviderType> providerTypeClass, Function<ComponentModel, StorageProviderModelType> toStorageProviderModelTypeFunction, String configScope) {
this.session = session;
this.providerTypeClass = providerTypeClass;
this.factoryTypeClass = factoryTypeClass;
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);
}
/**
* Returns stream of all storageProviders within the realm that implements the capabilityInterface.
*
* @param realm realm
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @return enabled storage providers for realm and @{code getProviderTypeClass()}
*/
protected <T> Stream<T> getEnabledStorageProviders(RealmModel realm, Class<T> capabilityInterface) {
return getStorageProviderModels(realm, providerTypeClass)
.map(toStorageProviderModelTypeFunction)
.filter(StorageProviderModelType::isEnabled)
.sorted(StorageProviderModelType.comparator)
.map(storageProviderModelType -> getStorageProviderInstance(storageProviderModelType, capabilityInterface, false))
.filter(Objects::nonNull);
}
/**
* Gets all enabled StorageProviders that implements the capabilityInterface, 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 capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @param applyFunction function that is applied on StorageProviders
* @param <R> result of applyFunction
* @return a stream with all results from all StorageProviders
*/
protected <R, T> Stream<R> flatMapEnabledStorageProvidersWithTimeout(RealmModel realm, Class<T> capabilityInterface, Function<T, ? extends Stream<R>> applyFunction) {
return getEnabledStorageProviders(realm, capabilityInterface)
.flatMap(ServicesUtils.timeBound(session, getStorageProviderTimeout(), applyFunction));
}
/**
* Gets all enabled StorageProviders that implements the capabilityInterface, applies applyFunction on each of
* them and returns the stream.
*
* !! Each StorageProvider has a limited time to respond, if it fails to do it, null is returned !!
*
* @param realm realm
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @param applyFunction function that is applied on StorageProviders
* @param <R> Result of applyFunction
* @return First result from StorageProviders
*/
protected <R, T> Stream<R> mapEnabledStorageProvidersWithTimeout(RealmModel realm, Class<T> capabilityInterface, Function<T, R> applyFunction) {
return getEnabledStorageProviders(realm, capabilityInterface)
.map(ServicesUtils.timeBoundOne(session, getStorageProviderTimeout(), applyFunction))
.filter(Objects::nonNull);
}
/**
* Gets all enabled StorageProviders that implements the capabilityInterface and call applyFunction on each
*
* !! Each StorageProvider has a limited time for consuming !!
*
* @param realm realm
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @param consumer function that is applied on StorageProviders
*/
protected <T> void consumeEnabledStorageProvidersWithTimeout(RealmModel realm, Class<T> capabilityInterface, Consumer<T> consumer) {
getEnabledStorageProviders(realm, capabilityInterface)
.forEachOrdered(ServicesUtils.consumeWithTimeBound(session, getStorageProviderTimeout(), consumer));
}
protected <T> T getStorageProviderInstance(RealmModel realm, String providerId, Class<T> capabilityInterface) {
return getStorageProviderInstance(realm, providerId, capabilityInterface, false);
}
/**
* Returns an instance of provider with the providerId within the realm or null if storage provider with providerId
* doesn't implement capabilityInterface.
*
* @param realm realm
* @param providerId id of ComponentModel within database/storage
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @return an instance of type CreatedProviderType or null if storage provider with providerId doesn't implement capabilityInterface
*/
protected <T> T getStorageProviderInstance(RealmModel realm, String providerId, Class<T> capabilityInterface, boolean includeDisabled) {
if (providerId == null || capabilityInterface == null) return null;
return getStorageProviderInstance(getStorageProviderModel(realm, providerId), capabilityInterface, includeDisabled);
}
/**
* Returns an instance of StorageProvider model corresponding realm and providerId
* @param realm Realm.
* @param providerId Id of desired provider.
* @return An instance of type StorageProviderModelType
*/
protected StorageProviderModelType getStorageProviderModel(RealmModel realm, String providerId) {
ComponentModel componentModel = realm.getComponent(providerId);
if (componentModel == null) {
return null;
}
return toStorageProviderModelTypeFunction.apply(componentModel);
}
/**
* Returns an instance of provider for the model or null if storage provider based on the model doesn't implement capabilityInterface.
*
* @param model StorageProviderModel obtained from database/storage
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @param <T> Required capability interface type
* @return an instance of type T or null if storage provider based on the model doesn't exist or doesn't implement the capabilityInterface.
*/
protected <T> T getStorageProviderInstance(StorageProviderModelType model, Class<T> capabilityInterface) {
return getStorageProviderInstance(model, capabilityInterface, false);
}
/**
* Returns an instance of provider for the model or null if storage provider based on the model doesn't implement capabilityInterface.
*
* @param model StorageProviderModel obtained from database/storage
* @param capabilityInterface class of desired capabilityInterface.
* For example, {@code GroupLookupProvider} or {@code UserQueryProvider}
* @param includeDisabled If set to true, the method will return also disabled providers.
* @return an instance of type T or null if storage provider based on the model doesn't exist or doesn't implement the capabilityInterface.
*/
protected <T> T getStorageProviderInstance(StorageProviderModelType model, Class<T> capabilityInterface, boolean includeDisabled) {
if (model == null || (!model.isEnabled() && !includeDisabled) || capabilityInterface == null) {
return null;
}
@SuppressWarnings("unchecked")
ProviderType instance = (ProviderType) session.getAttribute(model.getId());
if (instance != null && capabilityInterface.isAssignableFrom(instance.getClass())) return capabilityInterface.cast(instance);
ComponentFactory<? extends ProviderType, ProviderType> factory = getStorageProviderFactory(model.getProviderId());
if (factory == null) {
LOG.warnv("Configured StorageProvider {0} of provider id {1} does not exist", model.getName(), model.getProviderId());
return null;
}
if (!Types.supports(capabilityInterface, factory, factoryTypeClass)) {
return null;
}
instance = factory.create(session, model);
if (instance == null) {
throw new IllegalStateException("StorageProviderFactory (of type " + factory.getClass().getName() + ") produced a null instance");
}
session.enlistForClose(instance);
session.setAttribute(model.getId(), instance);
return capabilityInterface.cast(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);
}
}
@@ -0,0 +1,274 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.PrioritizedComponentModel;
import org.keycloak.models.cache.CachedObject;
import java.util.Calendar;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CacheableStorageProviderModel extends PrioritizedComponentModel {
public static final String CACHE_POLICY = "cachePolicy";
public static final String MAX_LIFESPAN = "maxLifespan";
public static final String EVICTION_HOUR = "evictionHour";
public static final String EVICTION_MINUTE = "evictionMinute";
public static final String EVICTION_DAY = "evictionDay";
public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
public static final String ENABLED = "enabled";
private transient CachePolicy cachePolicy;
private transient long maxLifespan = -1;
private transient int evictionHour = -1;
private transient int evictionMinute = -1;
private transient int evictionDay = -1;
private transient long cacheInvalidBefore = -1;
private transient Boolean enabled;
public CacheableStorageProviderModel() {
}
public CacheableStorageProviderModel(ComponentModel copy) {
super(copy);
}
public CachePolicy getCachePolicy() {
if (cachePolicy == null) {
String str = getConfig().getFirst(CACHE_POLICY);
if (str == null) return null;
cachePolicy = CachePolicy.valueOf(str);
}
return cachePolicy;
}
public void setCachePolicy(CachePolicy cachePolicy) {
this.cachePolicy = cachePolicy;
if (cachePolicy == null) {
getConfig().remove(CACHE_POLICY);
} else {
getConfig().putSingle(CACHE_POLICY, cachePolicy.name());
}
}
public long getMaxLifespan() {
if (maxLifespan < 0) {
String str = getConfig().getFirst(MAX_LIFESPAN);
if (str == null) return -1;
maxLifespan = Long.valueOf(str);
}
return maxLifespan;
}
public void setMaxLifespan(long maxLifespan) {
this.maxLifespan = maxLifespan;
getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan));
}
public int getEvictionHour() {
if (evictionHour < 0) {
String str = getConfig().getFirst(EVICTION_HOUR);
if (str == null) return -1;
evictionHour = Integer.valueOf(str);
}
return evictionHour;
}
public void setEvictionHour(int evictionHour) {
if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23");
this.evictionHour = evictionHour;
getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour));
}
public int getEvictionMinute() {
if (evictionMinute < 0) {
String str = getConfig().getFirst(EVICTION_MINUTE);
if (str == null) return -1;
evictionMinute = Integer.valueOf(str);
}
return evictionMinute;
}
public void setEvictionMinute(int evictionMinute) {
if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59");
this.evictionMinute = evictionMinute;
getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute));
}
public int getEvictionDay() {
if (evictionDay < 0) {
String str = getConfig().getFirst(EVICTION_DAY);
if (str == null) return -1;
evictionDay = Integer.valueOf(str);
}
return evictionDay;
}
public void setEvictionDay(int evictionDay) {
if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7");
this.evictionDay = evictionDay;
getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay));
}
public long getCacheInvalidBefore() {
if (cacheInvalidBefore < 0) {
String str = getConfig().getFirst(CACHE_INVALID_BEFORE);
if (str == null) return -1;
cacheInvalidBefore = Long.valueOf(str);
}
return cacheInvalidBefore;
}
public void setCacheInvalidBefore(long cacheInvalidBefore) {
this.cacheInvalidBefore = cacheInvalidBefore;
getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore));
}
public void setEnabled(boolean flag) {
enabled = flag;
getConfig().putSingle(ENABLED, Boolean.toString(flag));
}
public boolean isEnabled() {
if (enabled == null) {
String val = getConfig().getFirst(ENABLED);
if (val == null) {
enabled = true;
} else {
enabled = Boolean.valueOf(val);
}
}
return enabled;
}
public long getLifespan() {
CachePolicy policy = getCachePolicy();
long lifespan = -1;
if (policy == null || policy == CachePolicy.DEFAULT) {
lifespan = -1;
} else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) {
if (getEvictionHour() > -1 && getEvictionMinute() > -1) {
lifespan = dailyTimeout(getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis();
}
} else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
if (getEvictionDay() > 0 && getEvictionHour() > -1 && getEvictionMinute() > -1) {
lifespan = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis();
}
} else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
lifespan = getMaxLifespan();
}
return lifespan;
}
public boolean shouldInvalidate(CachedObject cached) {
boolean invalidate = false;
if (!isEnabled()) {
invalidate = true;
} else {
CacheableStorageProviderModel.CachePolicy policy = getCachePolicy();
if (policy != null) {
//String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis()));
if (policy == CacheableStorageProviderModel.CachePolicy.NO_CACHE) {
invalidate = true;
} else if (cached.getCacheTimestamp() < getCacheInvalidBefore()) {
invalidate = true;
} else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
if (cached.getCacheTimestamp() + getMaxLifespan() < Time.currentTimeMillis()) {
invalidate = true;
}
} else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) {
long dailyBoundary = dailyEvictionBoundary(getEvictionHour(), getEvictionMinute());
if (cached.getCacheTimestamp() <= dailyBoundary) {
invalidate = true;
}
} else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
int oneWeek = 7 * 24 * 60 * 60 * 1000;
long weeklyTimeout = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute());
long lastTimeout = weeklyTimeout - oneWeek;
//String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout));
//String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
if (cached.getCacheTimestamp() <= lastTimeout) {
invalidate = true;
}
}
}
}
return invalidate;
}
public static long dailyTimeout(int hour, int minute) {
Calendar cal = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal.setTimeInMillis(Time.currentTimeMillis());
cal2.setTimeInMillis(Time.currentTimeMillis());
cal2.set(Calendar.HOUR_OF_DAY, hour);
cal2.set(Calendar.MINUTE, minute);
if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
int add = (24 * 60 * 60 * 1000);
cal.add(Calendar.MILLISECOND, add);
} else {
cal = cal2;
}
return cal.getTimeInMillis();
}
public static long dailyEvictionBoundary(int hour, int minute) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(Time.currentTimeMillis());
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
if (cal.getTimeInMillis() > Time.currentTimeMillis()) {
// if daily evict for today hasn't happened yet set boundary
// to yesterday's time of eviction
cal.add(Calendar.DAY_OF_YEAR, -1);
}
return cal.getTimeInMillis();
}
public static long weeklyTimeout(int day, int hour, int minute) {
Calendar cal = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal.setTimeInMillis(Time.currentTimeMillis());
cal2.setTimeInMillis(Time.currentTimeMillis());
cal2.set(Calendar.HOUR_OF_DAY, hour);
cal2.set(Calendar.MINUTE, minute);
cal2.set(Calendar.DAY_OF_WEEK, day);
if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
int add = (7 * 24 * 60 * 60 * 1000);
cal2.add(Calendar.MILLISECOND, add);
}
return cal2.getTimeInMillis();
}
public enum CachePolicy {
NO_CACHE,
DEFAULT,
EVICT_DAILY,
EVICT_WEEKLY,
MAX_LIFESPAN
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.RoleProvider;
import org.keycloak.models.UserProvider;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
public interface StoreManagers {
ClientProvider clientStorageManager();
ClientScopeProvider clientScopeStorageManager();
RoleProvider roleStorageManager();
GroupProvider groupStorageManager();
UserProvider userStorageManager();
UserProvider userLocalStorage();
UserFederatedStorageProvider userFederatedStorage();
}
@@ -0,0 +1,103 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.provider.Provider;
/**
* A class implementing this interface represents a user storage provider to Keycloak.
* <p/>
* This interface contains only very basic methods for manipulating users. However, the storage provider capabilities
* are extended by implementing one or more of the following capability interfaces:
* <ul>
* <li>{@link org.keycloak.storage.user.UserLookupProvider UserLookupProvider} - Provide basic lookup methods. After implementing it is possible to login using users from the storage.</li>
* <li>{@link org.keycloak.storage.user.UserQueryMethodsProvider UserQueryMethodsProvider} - Provide complex lookup methods. After implementing it is possible to manage users from admin console.</li>
* <li>{@link org.keycloak.storage.user.UserCountMethodsProvider UserCountMethodsProvider} - Provide complex count methods. After implementing it is possible to leverage optimizations during querying for users.</li>
* <li>{@link org.keycloak.storage.user.UserQueryProvider UserQueryProvider} - This interface is combined capability of {@code UserQueryMethodsProvider} and {@code UserCountMethodsProvider}.</li>
* <li>{@link org.keycloak.storage.user.UserRegistrationProvider UserRegistrationProvider} - Provide methods for adding users. After implementing it is possible to store registered users in the storage.</li>
* <li>{@link org.keycloak.storage.user.UserBulkUpdateProvider UserBulkUpdateProvider} - After implementing it is possible to perform bulk operations on all users from storage (for example, addition of a role to all users).</li>
* <li>{@link org.keycloak.storage.user.ImportedUserValidation ImportedUserValidation} - Provider method for validating users within Keycloak local storage that are imported from the storage.</li>
* <li>{@link org.keycloak.storage.user.ImportSynchronization ImportSynchronization} - Provider methods for synchronization of the storage with Keycloak local storage. After implementing it is possible to sync users in the Admin console.</li>
* </ul>
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserStorageProvider extends Provider {
/**
* Callback when a realm is removed. Implement this if, for example, you want to do some
* cleanup in your user storage when a realm is removed
*
* @param realm
*/
default
void preRemove(RealmModel realm) {
}
/**
* Callback when a group is removed. Allows you to do things like remove a user
* group mapping in your external store if appropriate
*
* @param realm
* @param group
*/
default
void preRemove(RealmModel realm, GroupModel group) {
}
/**
* Callback when a role is removed. Allows you to do things like remove a user
* role mapping in your external store if appropriate
* @param realm
* @param role
*/
default
void preRemove(RealmModel realm, RoleModel role) {
}
/**
* Optional type that can be used by implementations to
* describe edit mode of user storage
*
*/
enum EditMode {
/**
* user storage is read-only
*/
READ_ONLY,
/**
* user storage is writable
*
*/
WRITABLE,
/**
* updates to user are stored locally and not synced with user storage.
*
*/
UNSYNCED
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
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 org.keycloak.storage.user.ImportSynchronization;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> {
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
T create(KeycloakSession session, ComponentModel model);
/**
* This is the name of the provider and will be showed in the admin console as an option.
*
* @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.emptyList();
}
@Override
default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}
/**
* Called when UserStorageProviderModel is created. This allows you to do initialization of any additional configuration
* you need to add. For example, you may be introspecting a database or ldap schema to automatically create mappings.
*
* @param session
* @param realm
* @param model
*/
@Override
default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
/**
* configuration properties that are common across all UserStorageProvider implementations
*
* @return
*/
@Override
default
List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return UserStorageProviderSpi.commonConfig();
}
@Override
default
Map<String, Object> getTypeMetadata() {
Map<String, Object> metadata = new HashMap<>();
if (this instanceof ImportSynchronization) {
metadata.put("synchronizable", true);
}
return metadata;
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import org.keycloak.component.ComponentModel;
/**
* Stored configuration of a User Storage provider instance.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
public class UserStorageProviderModel extends CacheableStorageProviderModel {
public static final String IMPORT_ENABLED = "importEnabled";
public static final String FULL_SYNC_PERIOD = "fullSyncPeriod";
public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod";
public static final String LAST_SYNC = "lastSync";
public UserStorageProviderModel() {
setProviderType(UserStorageProvider.class.getName());
}
public UserStorageProviderModel(ComponentModel copy) {
super(copy);
}
private transient Integer fullSyncPeriod;
private transient Integer changedSyncPeriod;
private transient Integer lastSync;
private transient Boolean importEnabled;
public boolean isImportEnabled() {
if (importEnabled == null) {
String val = getConfig().getFirst(IMPORT_ENABLED);
if (val == null) {
importEnabled = true;
} else {
importEnabled = Boolean.valueOf(val);
}
}
return importEnabled;
}
public void setImportEnabled(boolean flag) {
importEnabled = flag;
getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag));
}
public int getFullSyncPeriod() {
if (fullSyncPeriod == null) {
String val = getConfig().getFirst(FULL_SYNC_PERIOD);
if (val == null) {
fullSyncPeriod = -1;
} else {
fullSyncPeriod = Integer.valueOf(val);
}
}
return fullSyncPeriod;
}
public void setFullSyncPeriod(int fullSyncPeriod) {
this.fullSyncPeriod = fullSyncPeriod;
getConfig().putSingle(FULL_SYNC_PERIOD, Integer.toString(fullSyncPeriod));
}
public int getChangedSyncPeriod() {
if (changedSyncPeriod == null) {
String val = getConfig().getFirst(CHANGED_SYNC_PERIOD);
if (val == null) {
changedSyncPeriod = -1;
} else {
changedSyncPeriod = Integer.valueOf(val);
}
}
return changedSyncPeriod;
}
public void setChangedSyncPeriod(int changedSyncPeriod) {
this.changedSyncPeriod = changedSyncPeriod;
getConfig().putSingle(CHANGED_SYNC_PERIOD, Integer.toString(changedSyncPeriod));
}
public int getLastSync() {
if (lastSync == null) {
String val = getConfig().getFirst(LAST_SYNC);
if (val == null) {
lastSync = 0;
} else {
lastSync = Integer.valueOf(val);
}
}
return lastSync;
}
public void setLastSync(int lastSync) {
this.lastSync = lastSync;
getConfig().putSingle(LAST_SYNC, Integer.toString(lastSync));
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
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;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "storage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return UserStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return UserStorageProviderFactory.class;
}
private static final List<ProviderConfigProperty> commonConfig;
static {
List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
.property()
.name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
.property()
.name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("fullSyncPeriod").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("changedSyncPeriod").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("lastSync").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("batchSizeForSync").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("importEnabled").type(ProviderConfigProperty.BOOLEAN_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;
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.cache.UserCache;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
/**
* @author Alexander Schwartz
*/
public class UserStorageUtil {
public static UserFederatedStorageProvider userFederatedStorage(KeycloakSession session) {
return session.getProvider(UserFederatedStorageProvider.class);
}
public static UserCache userCache(KeycloakSession session) {
return session.getProvider(UserCache.class);
}
}
@@ -0,0 +1,514 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.adapter;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.StorageId;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This abstract class provides implementations for everything but getUsername(). getId() returns a default value
* of "f:" + providerId + ":" + getUsername(). isEnabled() returns true. getRoleMappings() will return default roles.
* getGroups() will return default groups.
*
* All other read methods return null, an empty collection, or false depending
* on the type. All update methods throw a ReadOnlyException.
*
* Provider implementors should override the methods for attributes, properties, and mappings they support.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractUserAdapter extends UserModelDefaultMethods {
protected KeycloakSession session;
protected RealmModel realm;
protected ComponentModel storageProviderModel;
public AbstractUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel storageProviderModel) {
this.session = session;
this.realm = realm;
this.storageProviderModel = storageProviderModel;
}
/**
* @deprecated User {@link #getRequiredActionsStream()}
*/
public Set<String> getRequiredActions() {
return Collections.emptySet();
}
@Override
public Stream<String> getRequiredActionsStream() {
return getRequiredActions().stream();
}
@Override
public void addRequiredAction(String action) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void removeRequiredAction(String action) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void addRequiredAction(RequiredAction action) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void removeRequiredAction(RequiredAction action) {
throw new ReadOnlyException("user is read only for this update");
}
/**
* Get group membership mappings that are managed by this storage provider
*
* @return
*/
protected Set<GroupModel> getGroupsInternal() {
return Collections.emptySet();
}
/**
* Should the realm's default groups be appended to getGroups() call?
* If your storage provider is not managing group mappings then it is recommended that
* this method return true
*
* @return
*/
protected boolean appendDefaultGroups() {
return true;
}
/**
* @deprecated Use {@link #getGroupsStream()} instead
*/
public Set<GroupModel> getGroups() {
Set<GroupModel> set = new HashSet<>();
if (appendDefaultGroups()) set.addAll(realm.getDefaultGroupsStream().collect(Collectors.toSet()));
set.addAll(getGroupsInternal());
return set;
}
@Override
public Stream<GroupModel> getGroupsStream() {
return getGroups().stream();
}
@Override
public void joinGroup(GroupModel group) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void leaveGroup(GroupModel group) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public boolean isMemberOf(GroupModel group) {
return RoleUtils.isMember(getGroups().stream(), group);
}
/**
*
* @deprecated Use {@link #getRealmRoleMappingsStream()} instead
*/
public Set<RoleModel> getRealmRoleMappings() {
return getRoleMappings().stream().filter(RoleUtils::isRealmRole).collect(Collectors.toSet());
}
@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
return getRealmRoleMappings().stream();
}
/**
*
* @deprecated Use {@link #getClientRoleMappingsStream(ClientModel)} instead
*/
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
return getRoleMappings().stream().filter(r -> RoleUtils.isClientRole(r, app)).collect(Collectors.toSet());
}
@Override
public Stream<RoleModel> getClientRoleMappingsStream(ClientModel app) {
return getClientRoleMappings(app).stream();
}
@Override
public boolean hasRole(RoleModel role) {
return RoleUtils.hasRole(getRoleMappings().stream(), role)
|| RoleUtils.hasRoleFromGroup(getGroups().stream(), role, true);
}
@Override
public void grantRole(RoleModel role) {
throw new ReadOnlyException("user is read only for this update");
}
/**
* Should the realm's default roles be appended to getRoleMappings() call?
* If your storage provider is not managing all role mappings then it is recommended that
* this method return true
*
* @return
*/
protected boolean appendDefaultRolesToRoleMappings() {
return true;
}
protected Set<RoleModel> getRoleMappingsInternal() {
return Collections.emptySet();
}
/**
*
* @deprecated Use {@link #getRoleMappingsStream()} instead
*/
public Set<RoleModel> getRoleMappings() {
Set<RoleModel> set = new HashSet<>();
if (appendDefaultRolesToRoleMappings()) set.addAll(realm.getDefaultRole().getCompositesStream().collect(Collectors.toSet()));
set.addAll(getRoleMappingsInternal());
return set;
}
@Override
public Stream<RoleModel> getRoleMappingsStream() {
return getRoleMappings().stream();
}
@Override
public void deleteRoleMapping(RoleModel role) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void setEnabled(boolean enabled) {
throw new ReadOnlyException("user is read only for this update");
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public String getFederationLink() {
return null;
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public void setFederationLink(String link) {
throw new ReadOnlyException("user is read only for this update");
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public String getServiceAccountClientLink() {
return null;
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public void setServiceAccountClientLink(String clientInternalId) {
throw new ReadOnlyException("user is read only for this update");
}
protected StorageId storageId;
/**
* Defaults to 'f:' + storageProvider.getId() + ':' + getUsername()
*
* @return
*/
@Override
public String getId() {
if (storageId == null) {
storageId = new StorageId(storageProviderModel.getId(), getUsername());
}
return storageId.getId();
}
@Override
public void setUsername(String username) {
throw new ReadOnlyException("user is read only for this update");
}
protected long created = System.currentTimeMillis();
@Override
public Long getCreatedTimestamp() {
return created;
}
@Override
public void setCreatedTimestamp(Long timestamp) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void setSingleAttribute(String name, String value) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void removeAttribute(String name) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public void setAttribute(String name, List<String> values) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public String getFirstAttribute(String name) {
if (name.equals(UserModel.USERNAME)) {
return getUsername();
}
return null;
}
@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
attributes.add(UserModel.USERNAME, getUsername());
return attributes;
}
/**
* @deprecated Use {@link #getAttributeStream(String)} instead
*/
public List<String> getAttribute(String name) {
if (name.equals(UserModel.USERNAME)) {
return Collections.singletonList(getUsername());
}
return Collections.emptyList();
}
@Override
public Stream<String> getAttributeStream(String name) {
return getAttribute(name).stream();
}
@Override
public String getFirstName() {
return null;
}
@Override
public void setFirstName(String firstName) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public String getLastName() {
return null;
}
@Override
public void setLastName(String lastName) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public String getEmail() {
return null;
}
@Override
public void setEmail(String email) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public boolean isEmailVerified() {
return false;
}
@Override
public void setEmailVerified(boolean verified) {
throw new ReadOnlyException("user is read only for this update");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
UserModel that = (UserModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
/**
* The {@link Streams} interface makes all collection-based methods in {@link AbstractUserAdapter} default by providing
* implementations that delegate to the {@link Stream}-based variants instead of the other way around.
* <p/>
* It allows for implementations to focus on the {@link Stream}-based approach for processing sets of data and benefit
* from the potential memory and performance optimizations of that approach.
*/
public abstract static class Streams extends AbstractUserAdapter implements UserModel {
public Streams(final KeycloakSession session, final RealmModel realm, final ComponentModel storageProviderModel) {
super(session, realm, storageProviderModel);
}
@Override
public Set<String> getRequiredActions() {
return this.getRequiredActionsStream().collect(Collectors.toSet());
}
@Override
public Stream<String> getRequiredActionsStream() {
return Stream.empty();
}
@Override
public List<String> getAttribute(String name) {
return this.getAttributeStream(name).collect(Collectors.toList());
}
@Override
public Stream<String> getAttributeStream(String name) {
if (name.equals(UserModel.USERNAME)) {
return Stream.of(getUsername());
}
return Stream.empty();
}
// group-related methods.
@Override
public Set<GroupModel> getGroups() {
return this.getGroupsStream().collect(Collectors.toSet());
}
@Override
public Stream<GroupModel> getGroupsStream() {
Stream<GroupModel> groups = getGroupsInternal().stream();
if (appendDefaultGroups()) groups = Stream.concat(groups, realm.getDefaultGroupsStream());
return groups;
}
@Override
public boolean isMemberOf(GroupModel group) {
return RoleUtils.isMember(this.getGroupsStream(), group);
}
// role-related methods.
@Override
public Set<RoleModel> getRealmRoleMappings() {
return this.getRealmRoleMappingsStream().collect(Collectors.toSet());
}
@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
return getRoleMappingsStream().filter(RoleUtils::isRealmRole);
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
return this.getClientRoleMappingsStream(app).collect(Collectors.toSet());
}
@Override
public Stream<RoleModel> getClientRoleMappingsStream(ClientModel app) {
return getRoleMappingsStream().filter(r -> RoleUtils.isClientRole(r, app));
}
@Override
public Set<RoleModel> getRoleMappings() {
return this.getRoleMappingsStream().collect(Collectors.toSet());
}
@Override
public Stream<RoleModel> getRoleMappingsStream() {
Stream<RoleModel> roleMappings = getRoleMappingsInternal().stream();
if (appendDefaultRolesToRoleMappings()) return Stream.concat(roleMappings, realm.getDefaultRole().getCompositesStream());
return roleMappings;
}
@Override
public boolean hasRole(RoleModel role) {
return RoleUtils.hasRole(this.getRoleMappingsStream(), role)
|| RoleUtils.hasRoleFromGroup(this.getGroupsStream(), role, true);
}
}
}
@@ -0,0 +1,431 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.adapter;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.UserCredentialManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SubjectCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageUtil;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
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;
/**
* Assumes everything is managed by federated storage except for username. getId() returns a default value
* of "f:" + providerId + ":" + getUsername(). UserModel properties like enabled, firstName, lastName, email, etc. are all
* stored as attributes in federated storage.
*
* isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated storage
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractUserAdapterFederatedStorage extends UserModelDefaultMethods {
public static String FIRST_NAME_ATTRIBUTE = "FIRST_NAME";
public static String LAST_NAME_ATTRIBUTE = "LAST_NAME";
public static String EMAIL_ATTRIBUTE = "EMAIL";
public static String EMAIL_VERIFIED_ATTRIBUTE = "EMAIL_VERIFIED";
public static String CREATED_TIMESTAMP_ATTRIBUTE = "CREATED_TIMESTAMP";
public static String ENABLED_ATTRIBUTE = "ENABLED";
protected KeycloakSession session;
protected RealmModel realm;
protected ComponentModel storageProviderModel;
public AbstractUserAdapterFederatedStorage(KeycloakSession session, RealmModel realm, ComponentModel storageProviderModel) {
this.session = session;
this.realm = realm;
this.storageProviderModel = storageProviderModel;
}
public UserFederatedStorageProvider getFederatedStorage() {
return UserStorageUtil.userFederatedStorage(session);
}
@Override
public Stream<String> getRequiredActionsStream() {
return getFederatedStorage().getRequiredActionsStream(realm, this.getId());
}
@Override
public void addRequiredAction(String action) {
getFederatedStorage().addRequiredAction(realm, this.getId(), action);
}
@Override
public void removeRequiredAction(String action) {
getFederatedStorage().removeRequiredAction(realm, this.getId(), action);
}
@Override
public void addRequiredAction(RequiredAction action) {
getFederatedStorage().addRequiredAction(realm, this.getId(), action.name());
}
@Override
public void removeRequiredAction(RequiredAction action) {
getFederatedStorage().removeRequiredAction(realm, this.getId(), action.name());
}
/**
* Get group membership mappings that are managed by this storage provider
*
* @return
*/
protected Set<GroupModel> getGroupsInternal() {
return Collections.emptySet();
}
/**
* Should the realm's default groups be appended to getGroups() call?
* If your storage provider is not managing group mappings then it is recommended that
* this method return true
*
* @return
*/
protected boolean appendDefaultGroups() {
return true;
}
/**
* Gets groups from federated storage and automatically appends default groups of realm.
* Also calls getGroupsInternal() method
* to pull group membership from provider. Implementors can override that method
*
*/
@Override
public Stream<GroupModel> getGroupsStream() {
Stream<GroupModel> groups = getFederatedStorage().getGroupsStream(realm, this.getId());
if (appendDefaultGroups()) groups = Stream.concat(groups, realm.getDefaultGroupsStream());
return Stream.concat(groups, getGroupsInternal().stream());
}
@Override
public void joinGroup(GroupModel group) {
getFederatedStorage().joinGroup(realm, this.getId(), group);
}
@Override
public void leaveGroup(GroupModel group) {
getFederatedStorage().leaveGroup(realm, this.getId(), group);
}
@Override
public boolean isMemberOf(GroupModel group) {
return RoleUtils.isMember(getGroupsStream(), group);
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*/
@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
return this.getRoleMappingsStream().filter(RoleUtils::isRealmRole);
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*/
@Override
public Stream<RoleModel> getClientRoleMappingsStream(ClientModel app) {
return getRoleMappingsStream().filter(r -> RoleUtils.isClientRole(r, app));
}
@Override
public boolean hasRole(RoleModel role) {
return RoleUtils.hasRole(getRoleMappingsStream(), role)
|| RoleUtils.hasRoleFromGroup(getGroupsStream(), role, true);
}
@Override
public void grantRole(RoleModel role) {
getFederatedStorage().grantRole(realm, this.getId(), role);
}
/**
* Should the realm's default roles be appended to getRoleMappings() call?
* If your storage provider is not managing all role mappings then it is recommended that
* this method return true
*
* @return
*/
protected boolean appendDefaultRolesToRoleMappings() {
return true;
}
protected Set<RoleModel> getRoleMappingsInternal() {
return Collections.emptySet();
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*/
@Override
public Stream<RoleModel> getRoleMappingsStream() {
Stream<RoleModel> roleMappings = getFederatedRoleMappingsStream();
if (appendDefaultRolesToRoleMappings()) {
roleMappings = Stream.concat(roleMappings, realm.getDefaultRole().getCompositesStream());
}
return Stream.concat(roleMappings, getRoleMappingsInternal().stream());
}
/**
* @deprecated Use {@link #getFederatedRoleMappingsStream()} instead
*/
@Deprecated
protected Set<RoleModel> getFederatedRoleMappings() {
return getFederatedRoleMappingsStream().collect(Collectors.toSet());
}
protected Stream<RoleModel> getFederatedRoleMappingsStream() {
return getFederatedStorage().getRoleMappingsStream(realm, this.getId());
}
@Override
public void deleteRoleMapping(RoleModel role) {
getFederatedStorage().deleteRoleMapping(realm, this.getId(), role);
}
@Override
public boolean isEnabled() {
String val = getFirstAttribute(ENABLED_ATTRIBUTE);
if (val == null) return true;
else return Boolean.valueOf(val);
}
@Override
public void setEnabled(boolean enabled) {
setSingleAttribute(ENABLED_ATTRIBUTE, Boolean.toString(enabled));
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public String getFederationLink() {
return null;
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public void setFederationLink(String link) {
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public String getServiceAccountClientLink() {
return null;
}
/**
* This method should not be overriden
*
* @return
*/
@Override
public void setServiceAccountClientLink(String clientInternalId) {
}
protected StorageId storageId;
/**
* Defaults to 'f:' + storageProvider.getId() + ':' + getUsername()
*
* @return
*/
@Override
public String getId() {
if (storageId == null) {
storageId = new StorageId(storageProviderModel.getId(), getUsername());
}
return storageId.getId();
}
@Override
public Long getCreatedTimestamp() {
String val = getFirstAttribute(CREATED_TIMESTAMP_ATTRIBUTE);
if (val == null) return null;
else return Long.valueOf(val);
}
@Override
public void setCreatedTimestamp(Long timestamp) {
if (timestamp == null) {
setSingleAttribute(CREATED_TIMESTAMP_ATTRIBUTE, null);
} else {
setSingleAttribute(CREATED_TIMESTAMP_ATTRIBUTE, Long.toString(timestamp));
}
}
@Override
public void setSingleAttribute(String name, String value) {
if (UserModel.USERNAME.equals(name)) {
setUsername(value);
} else {
getFederatedStorage().setSingleAttribute(realm, this.getId(), mapAttribute(name), value);
}
}
@Override
public void removeAttribute(String name) {
getFederatedStorage().removeAttribute(realm, this.getId(), name);
}
@Override
public void setAttribute(String name, List<String> values) {
if (UserModel.USERNAME.equals(name)) {
setUsername((values != null && !values.isEmpty()) ? values.get(0) : null);
} else {
getFederatedStorage().setAttribute(realm, this.getId(), mapAttribute(name), values);
}
}
@Override
public String getFirstAttribute(String name) {
if (UserModel.USERNAME.equals(name)) {
return getUsername();
}
return getFederatedStorage().getAttributes(realm, this.getId()).getFirst(mapAttribute(name));
}
@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> attributes = getFederatedStorage().getAttributes(realm, this.getId());
if (attributes == null) {
attributes = new MultivaluedHashMap<>();
}
List<String> firstName = attributes.remove(FIRST_NAME_ATTRIBUTE);
attributes.add(UserModel.FIRST_NAME, firstName != null && firstName.size() >= 1 ? firstName.get(0) : null);
List<String> lastName = attributes.remove(LAST_NAME_ATTRIBUTE);
attributes.add(UserModel.LAST_NAME, lastName != null && lastName.size() >= 1 ? lastName.get(0) : null);
List<String> email = attributes.remove(EMAIL_ATTRIBUTE);
attributes.add(UserModel.EMAIL, email != null && email.size() >= 1 ? email.get(0) : null);
attributes.add(UserModel.USERNAME, getUsername());
return attributes;
}
@Override
public Stream<String> getAttributeStream(String name) {
if (UserModel.USERNAME.equals(name)) {
return Stream.of(getUsername());
}
List<String> result = getFederatedStorage().getAttributes(realm, this.getId()).get(mapAttribute(name));
return (result == null) ? Stream.empty() : result.stream();
}
protected String mapAttribute(String attributeName) {
if (UserModel.FIRST_NAME.equals(attributeName)) {
return FIRST_NAME_ATTRIBUTE;
} else if (UserModel.LAST_NAME.equals(attributeName)) {
return LAST_NAME_ATTRIBUTE;
} else if (UserModel.EMAIL.equals(attributeName)) {
return EMAIL_ATTRIBUTE;
}
return attributeName;
}
@Override
public boolean isEmailVerified() {
String val = getFirstAttribute(EMAIL_VERIFIED_ATTRIBUTE);
if (val == null) return false;
else return Boolean.valueOf(val);
}
/**
* Stores as attribute in federated storage.
* EMAIL_VERIFIED_ATTRIBUTE
*
* @param verified
*/
@Override
public void setEmailVerified(boolean verified) {
setSingleAttribute(EMAIL_VERIFIED_ATTRIBUTE, Boolean.toString(verified));
}
@Override
public SubjectCredentialManager credentialManager() {
return new UserCredentialManager(session, realm, this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
UserModel that = (UserModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
public abstract static class Streams extends AbstractUserAdapterFederatedStorage implements UserModel {
public Streams(final KeycloakSession session, final RealmModel realm, final ComponentModel storageProviderModel) {
super(session, realm, storageProviderModel);
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.client;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.provider.Provider;
/**
* Base interface for components that want to provide an alternative storage mechanism for clients
*
* This is currently a private incomplete SPI. Please discuss on dev list if you want us to complete it or want to do the work yourself.
* This work is described in KEYCLOAK-6408 JIRA issue.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientStorageProvider extends Provider, ClientLookupProvider {
/**
* Callback when a realm is removed. Implement this if, for example, you want to do some
* cleanup in your user storage when a realm is removed
*
* @param realm
*/
default
void preRemove(RealmModel realm) {
}
/**
* Callback when a group is removed. Allows you to do things like remove a user
* group mapping in your external store if appropriate
*
* @param realm
* @param group
*/
default
void preRemove(RealmModel realm, GroupModel group) {
}
/**
* Callback when a role is removed. Allows you to do things like remove a user
* role mapping in your external store if appropriate
* @param realm
* @param role
*/
default
void preRemove(RealmModel realm, RoleModel role) {
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.client;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.CacheableStorageProviderModel;
/**
* Stored configuration of a Client Storage provider instance.
*
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
public class ClientStorageProviderModel extends CacheableStorageProviderModel {
public static final String ENABLED = "enabled";
public ClientStorageProviderModel() {
setProviderType(ClientStorageProvider.class.getName());
}
public ClientStorageProviderModel(ComponentModel copy) {
super(copy);
}
private transient Boolean enabled;
public void setEnabled(boolean flag) {
enabled = flag;
getConfig().putSingle(ENABLED, Boolean.toString(flag));
}
public boolean isEnabled() {
if (enabled == null) {
String val = getConfig().getFirst(ENABLED);
if (val == null) {
enabled = true;
} else {
enabled = Boolean.valueOf(val);
}
}
return enabled;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.RealmModel;
import java.util.List;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserAttributeFederatedStorage {
void setSingleAttribute(RealmModel realm, String userId, String name, String value);
void setAttribute(RealmModel realm, String userId, String name, List<String> values);
void removeAttribute(RealmModel realm, String userId, String name);
MultivaluedHashMap<String, String> getAttributes(RealmModel realm, String userId);
/**
* Searches for federated users that have an attribute with the specified {@code name} and {@code value}.
*
* @param realm a reference to the realm.
* @param name the attribute name.
* @param value the attribute value.
* @return a non-null {@link Stream} of users that match the search criteria.
*/
Stream<String> getUsersByUserAttributeStream(RealmModel realm, String name, String value);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserAttributeFederatedStorage {
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserBrokerLinkFederatedStorage {
String getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm);
void addFederatedIdentity(RealmModel realm, String userId, FederatedIdentityModel socialLink);
boolean removeFederatedIdentity(RealmModel realm, String userId, String socialProvider);
void preRemove(RealmModel realm, IdentityProviderModel provider);
void updateFederatedIdentity(RealmModel realm, String userId, FederatedIdentityModel federatedIdentityModel);
/**
* Obtains the identities of the federated user identified by {@code userId}.
*
* @param userId the user identifier.
* @param realm a reference to the realm.
* @return a non-null {@link Stream} of federated identities associated with the user.
*/
Stream<FederatedIdentityModel> getFederatedIdentitiesStream(String userId, RealmModel realm);
FederatedIdentityModel getFederatedIdentity(String userId, String socialProvider, RealmModel realm);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserBrokerLinkFederatedStorage {
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserConsentModel;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserConsentFederatedStorage {
void addConsent(RealmModel realm, String userId, UserConsentModel consent);
UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientInternalId);
/**
* Obtains the consents associated with the federated user identified by {@code userId}.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @return a non-null {@link Stream} of consents associated with the user.
*/
Stream<UserConsentModel> getConsentsStream(RealmModel realm, String userId);
void updateConsent(RealmModel realm, String userId, UserConsentModel consent);
boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId);
/**
* @deprecated This interface is no longer necessary, collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserConsentFederatedStorage {
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserFederatedStorageProvider extends Provider,
UserAttributeFederatedStorage,
UserBrokerLinkFederatedStorage,
UserConsentFederatedStorage,
UserNotBeforeFederatedStorage,
UserGroupMembershipFederatedStorage,
UserRequiredActionsFederatedStorage,
UserRoleMappingsFederatedStorage,
UserFederatedUserCredentialStore {
/**
* Obtains the ids of all federated users in the realm.
*
* @param realm a reference to the realm.
* @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 a non-null {@link Stream} of federated user ids.
*/
Stream<String> getStoredUsersStream(RealmModel realm, Integer first, Integer max);
int getStoredUsersCount(RealmModel realm);
void preRemove(RealmModel realm);
void preRemove(RealmModel realm, GroupModel group);
void preRemove(RealmModel realm, RoleModel role);
void preRemove(RealmModel realm, ClientModel client);
void preRemove(ProtocolMapperModel protocolMapper);
void preRemove(ClientScopeModel clientScope);
void preRemove(RealmModel realm, UserModel user);
void preRemove(RealmModel realm, ComponentModel model);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserFederatedStorageProvider,
UserAttributeFederatedStorage.Streams,
UserBrokerLinkFederatedStorage.Streams,
UserConsentFederatedStorage.Streams,
UserFederatedUserCredentialStore.Streams,
UserGroupMembershipFederatedStorage.Streams,
UserRequiredActionsFederatedStorage.Streams,
UserRoleMappingsFederatedStorage.Streams {
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserFederatedStorageProviderFactory extends ProviderFactory<UserFederatedStorageProvider> {
}
@@ -0,0 +1,49 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserFederatedStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "userFederatedStorage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return UserFederatedStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return UserFederatedStorageProviderFactory.class;
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserFederatedUserCredentialStore extends Provider {
void updateCredential(RealmModel realm, String userId, CredentialModel cred);
CredentialModel createCredential(RealmModel realm, String userId, CredentialModel cred);
boolean removeStoredCredential(RealmModel realm, String userId, String id);
CredentialModel getStoredCredentialById(RealmModel realm, String userId, String id);
/**
* Obtains the credentials associated with the federated user identified by {@code userId}.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @return a non-null {@link Stream} of credentials.
*/
Stream<CredentialModel> getStoredCredentialsStream(RealmModel realm, String userId);
/**
* Obtains the credentials of type {@code type} that are associated with the federated user identified by {@code userId}.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @param type the credential type.
* @return a non-null {@link Stream} of credentials.
*/
Stream<CredentialModel> getStoredCredentialsByTypeStream(RealmModel realm, String userId, String type);
CredentialModel getStoredCredentialByNameAndType(RealmModel realm, String userId, String name, String type);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserFederatedUserCredentialStore {
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserGroupMembershipFederatedStorage {
/**
* Obtains the groups associated with the federated user.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @return a non-null {@code Stream} of groups.
*/
Stream<GroupModel> getGroupsStream(RealmModel realm, String userId);
void joinGroup(RealmModel realm, String userId, GroupModel group);
void leaveGroup(RealmModel realm, String userId, GroupModel group);
/**
* Obtains the federated users that are members of the given {@code group} in the specified {@code realm}.
*
* @param realm a reference to the realm.
* @param group a reference to the group whose federated members are being searched.
* @param firstResult 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 a non-null {@code Stream} of federated user ids that are members of the group in the realm.
*/
Stream<String> getMembershipStream(RealmModel realm, GroupModel group, Integer firstResult, Integer max);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserGroupMembershipFederatedStorage {
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2017 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.federated;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface UserNotBeforeFederatedStorage {
void setNotBeforeForUser(RealmModel realm, String userId, int notBefore);
int getNotBeforeOfUser(RealmModel realm, String userId);
}
@@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.models.RealmModel;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserRequiredActionsFederatedStorage {
/**
* Obtains the names of required actions associated with the federated user identified by {@code userId}.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @return a non-null {@link Stream} of required action names.
*/
Stream<String> getRequiredActionsStream(RealmModel realm, String userId);
void addRequiredAction(RealmModel realm, String userId, String action);
void removeRequiredAction(RealmModel realm, String userId, String action);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserRequiredActionsFederatedStorage {
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.federated;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import java.util.stream.Stream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserRoleMappingsFederatedStorage {
void grantRole(RealmModel realm, String userId, RoleModel role);
/**
* Obtains the roles associated with the federated user identified by {@code userId}.
*
* @param realm a reference to the realm.
* @param userId the user identifier.
* @return a non-null {@code Stream} of roles.
*/
Stream<RoleModel> getRoleMappingsStream(RealmModel realm, String userId);
void deleteRoleMapping(RealmModel realm, String userId, RoleModel role);
/**
* Obtains the federated users that are members of the given {@code role} in the specified {@code realm}.
*
* @param realm a reference to the realm.
* @param role a reference to the role whose federated members are being searched.
* @param firstResult 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 a non-null {@code Stream} of federated user ids that are members of the role in the realm.
*/
Stream<String> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer max);
/**
* @deprecated This interface is no longer necessary; collection-based methods were removed from the parent interface
* and therefore the parent interface can be used directly
*/
@Deprecated
interface Streams extends UserRoleMappingsFederatedStorage {
}
}
@@ -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 {
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.user;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.util.Date;
/**
*
* This is an optional capability interface that is intended to be implemented by any
* {@link org.keycloak.storage.UserStorageProvider UserStorageProvider} that supports syncing users to keycloak local
* storage. You must implement this interface if you want to be able to use sync functionality within the Admin console.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ImportSynchronization {
SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
}
@@ -0,0 +1,42 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.user;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* This is an optional capability interface that is intended to be implemented by any
* {@link org.keycloak.storage.UserStorageProvider UserStorageProvider} that supports validating users. You must
* implement this interface if your storage imports users into the Keycloak local storage and you want to sync these
* users with your storage. The idea is, that whenever keycloak queries users imported from your storage, the method
* {@link #validate(RealmModel, UserModel) validate()} is called and if it returns null, the user is removed from
* local storage and reloaded from your storage by corresponding method.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ImportedUserValidation {
/**
* If this method returns null, then the user in local storage will be removed
*
* @param realm
* @param user
* @return null if user no longer valid
*/
UserModel validate(RealmModel realm, UserModel user);
}
@@ -0,0 +1,134 @@
/*
* 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.utils;
import org.jboss.logging.Logger;
import org.keycloak.executors.ExecutorsProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
/**
* Utility class for general helper methods used across the keycloak-services.
*/
public class ServicesUtils {
private static final Logger logger = Logger.getLogger(ServicesUtils.class);
public static <T, R> Function<? super T,? extends Stream<? extends R>> timeBound(KeycloakSession session,
long timeout,
Function<T, ? extends Stream<R>> func) {
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("storage-provider-threads");
return p -> {
// We are running another thread here, which serves as a time checking thread. When timeout is hit, the time
// checking thread will send interrupted flag to main thread, which can cause interruption of func execution.
// To support interruption func implementation should react to interrupt flag.
// If func doesn't check the interrupted flag, the execution won't be interrupted and can take more time
// than the threshold given by timeout variable
Future<?> timeCheckingThread = executor.submit(timeWarningRunnable(timeout, Thread.currentThread()));
try {
// We cannot run func in different than main thread, because main thread have, for example, EntityManager
// transaction context. If we run any operation on EntityManager in a different thread, it will fail
// with a transaction doesn't exist error
return func.apply(p);
} finally {
timeCheckingThread.cancel(true);
if (Thread.interrupted()) {
logger.warnf("Execution with object [%s] exceeded specified time limit %d. %s", p, timeout, getShortStackTrace());
}
}
};
}
public static <T, R> Function<? super T, R> timeBoundOne(KeycloakSession session,
long timeout,
Function<T, R> func) {
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("storage-provider-threads");
return p -> {
// We are running another thread here, which serves as a time checking thread. When timeout is hit, the time
// checking thread will send interrupted flag to main thread, which can cause interruption of func execution.
// To support interruption func implementation should react to interrupt flag.
// If func doesn't check the interrupted flag, the execution won't be interrupted and can take more time
// than the threshold given by timeout variable
Future<?> warningThreadFuture = executor.submit(timeWarningRunnable(timeout, Thread.currentThread()));
try {
// We cannot run func in different than main thread, because main thread have, for example, EntityManager
// transaction context. If we run any operation on EntityManager in a different thread, it will fail
// with a transaction doesn't exist error
return func.apply(p);
} finally {
warningThreadFuture.cancel(true);
if (Thread.interrupted()) {
logger.warnf("Execution with object [%s] exceeded specified time limit %d. %s", p, timeout, getShortStackTrace());
}
}
};
}
public static <T> Consumer<? super T> consumeWithTimeBound(KeycloakSession session,
long timeout,
Consumer<T> func) {
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("storage-provider-threads");
return p -> {
// We are running another thread here, which serves as a time checking thread. When timeout is hit, the time
// checking thread will send interrupted flag to main thread, which can cause interruption of func execution.
// To support interruption func implementation should react to interrupt flag.
// If func doesn't check the interrupted flag, the execution won't be interrupted and can take more time
// than the threshold given by timeout variable
Future<?> warningThreadFuture = executor.submit(timeWarningRunnable(timeout, Thread.currentThread()));
try {
// We cannot run func in different than main thread, because main thread have, for example, EntityManager
// transaction context. If we run any operation on EntityManager in a different thread, it will fail
// with a transaction doesn't exist error
func.accept(p);
} finally {
warningThreadFuture.cancel(true);
if (Thread.interrupted()) {
logger.warnf("Execution with object [%s] exceeded specified time limit %d. %s", p, timeout, getShortStackTrace());
}
}
};
}
private static Runnable timeWarningRunnable(long timeout, Thread mainThread) {
return new Runnable() {
@Override
public void run() {
try {
Thread.sleep(timeout);
} catch (InterruptedException exception) {
return; // Do not interrupt if warning thread was interrupted (== main thread finished execution in time)
}
mainThread.interrupt();
}
};
}
}
@@ -0,0 +1,19 @@
#
# Copyright 2022 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi