SingleUserCredentialManager moving in

- UserStorageManager now handles authentication for old Kerberos+LDAP style
- new getUserByCredential method in MapUserProvider would eventually do the same.
This commit is contained in:
Alexander Schwartz 2022-04-28 11:22:14 +02:00 committed by Hynek Mlnařík
parent 82094d113e
commit bc8fd21dc6
42 changed files with 1500 additions and 638 deletions

View file

@ -24,6 +24,7 @@ import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.CredentialValidationOutput;
@ -167,7 +168,7 @@ public class KerberosFederationProvider implements UserStorageProvider,
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
if (input.getType().equals(PasswordCredentialModel.TYPE) && !((LegacySingleUserCredentialManager) user.getUserCredentialManager()).isConfiguredLocally(PasswordCredentialModel.TYPE)) {
return validPassword(user.getUsername(), input.getChallengeResponse());
} else {
return false; // invalid cred type

View file

@ -75,6 +75,11 @@
<artifactId>jboss-transaction-api_1.3_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<!-- needed for InMemoryUserAdapter -->
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-legacy-private</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -36,6 +36,7 @@ import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.CredentialValidationOutput;
@ -712,7 +713,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
if (input.getType().equals(PasswordCredentialModel.TYPE) && !((LegacySingleUserCredentialManager) user.getUserCredentialManager()).isConfiguredLocally(PasswordCredentialModel.TYPE)) {
return validPassword(realm, user, input.getChallengeResponse());
} else {
return false; // invalid cred type

View file

@ -0,0 +1,120 @@
package org.keycloak.models.cache.infinispan;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import java.util.List;
import java.util.stream.Stream;
/**
* @author Alexander Schwartz
*/
public abstract class SingleUserCredentialManagerCacheAdapter implements SingleUserCredentialManager {
private final SingleUserCredentialManager singleUserCredentialManager;
protected SingleUserCredentialManagerCacheAdapter(SingleUserCredentialManager singleUserCredentialManager) {
this.singleUserCredentialManager = singleUserCredentialManager;
}
public abstract void invalidateCacheForUser();
@Override
public boolean isValid(List<CredentialInput> inputs) {
// validating a password might still update its hashes, similar logic might apply to OTP logic
// instead of having each
invalidateCacheForUser();
return singleUserCredentialManager.isValid(inputs);
}
@Override
public boolean updateCredential(CredentialInput input) {
invalidateCacheForUser();
return singleUserCredentialManager.updateCredential(input);
}
@Override
public void updateStoredCredential(CredentialModel cred) {
invalidateCacheForUser();
singleUserCredentialManager.updateStoredCredential(cred);
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
invalidateCacheForUser();
return singleUserCredentialManager.createStoredCredential(cred);
}
@Override
public boolean removeStoredCredentialById(String id) {
invalidateCacheForUser();
return singleUserCredentialManager.removeStoredCredentialById(id);
}
@Override
public CredentialModel getStoredCredentialById(String id) {
return singleUserCredentialManager.getStoredCredentialById(id);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
return singleUserCredentialManager.getStoredCredentialsStream();
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return singleUserCredentialManager.getStoredCredentialsByTypeStream(type);
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
return singleUserCredentialManager.getStoredCredentialByNameAndType(name, type);
}
@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
invalidateCacheForUser();
return singleUserCredentialManager.moveStoredCredentialTo(id, newPreviousCredentialId);
}
@Override
public void updateCredentialLabel(String credentialId, String userLabel) {
invalidateCacheForUser();
singleUserCredentialManager.updateCredentialLabel(credentialId, userLabel);
}
@Override
public void disableCredentialType(String credentialType) {
invalidateCacheForUser();
singleUserCredentialManager.disableCredentialType(credentialType);
}
@Override
public Stream<String> getDisableableCredentialTypesStream() {
return singleUserCredentialManager.getDisableableCredentialTypesStream();
}
@Override
public boolean isConfiguredFor(String type) {
return singleUserCredentialManager.isConfiguredFor(type);
}
@Override
public boolean isConfiguredLocally(String type) {
return singleUserCredentialManager.isConfiguredLocally(type);
}
@Override
public Stream<String> getConfiguredUserStorageCredentialTypesStream(UserModel user) {
return singleUserCredentialManager.getConfiguredUserStorageCredentialTypesStream(user);
}
@Override
public CredentialModel createCredentialThroughProvider(CredentialModel model) {
invalidateCacheForUser();
return singleUserCredentialManager.createCredentialThroughProvider(model);
}
}

View file

@ -17,11 +17,13 @@
package org.keycloak.models.cache.infinispan;
import org.keycloak.credential.CredentialModel;
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.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.infinispan.entities.CachedUser;
@ -33,6 +35,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
@ -50,6 +53,7 @@ public class UserAdapter implements CachedUserModel.Streams {
protected final KeycloakSession keycloakSession;
protected final RealmModel realm;
protected volatile UserModel updated;
private boolean userRegisteredForInvalidation;
public UserAdapter(CachedUser cached, UserCacheSession userProvider, KeycloakSession keycloakSession, RealmModel realm) {
this.cached = cached;
@ -97,8 +101,12 @@ public class UserAdapter implements CachedUserModel.Streams {
public UserModel getDelegateForUpdate() {
if (updated == null) {
userProviderCache.registerUserInvalidation(realm, cached);
userRegisteredForInvalidation = true;
updated = modelSupplier.get();
if (updated == null) throw new IllegalStateException("Not found in database");
} else if (!userRegisteredForInvalidation) {
userProviderCache.registerUserInvalidation(realm, cached);
userRegisteredForInvalidation = true;
}
return updated;
}
@ -278,6 +286,59 @@ public class UserAdapter implements CachedUserModel.Streams {
updated.setServiceAccountClientLink(clientInternalId);
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
if (updated == null) {
updated = modelSupplier.get();
if (updated == null) throw new IllegalStateException("Not found in database");
}
return new SingleUserCredentialManagerCacheAdapter(updated.getUserCredentialManager()) {
@Override
public CredentialModel getStoredCredentialById(String id) {
if (!userRegisteredForInvalidation) {
return cached.getStoredCredentials(modelSupplier).stream().filter(credential ->
Objects.equals(id, credential.getId()))
.findFirst().orElse(null);
}
return super.getStoredCredentialById(id);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
if (!userRegisteredForInvalidation) {
return cached.getStoredCredentials(modelSupplier).stream();
}
return super.getStoredCredentialsStream();
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
if (!userRegisteredForInvalidation) {
return cached.getStoredCredentials(modelSupplier).stream().filter(credential -> Objects.equals(type, credential.getType()));
}
return super.getStoredCredentialsByTypeStream(type);
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
if (!userRegisteredForInvalidation) {
return cached.getStoredCredentials(modelSupplier).stream().filter(credential ->
Objects.equals(type, credential.getType()) && Objects.equals(name, credential.getUserLabel()))
.findFirst().orElse(null);
}
return super.getStoredCredentialByNameAndType(name, type);
}
@Override
public void invalidateCacheForUser() {
if (!userRegisteredForInvalidation) {
userProviderCache.registerUserInvalidation(realm, cached);
userRegisteredForInvalidation = true;
}
}
};
}
@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
if (updated != null) return updated.getRealmRoleMappingsStream();
@ -392,4 +453,5 @@ public class UserAdapter implements CachedUserModel.Streams {
private UserModel getUserModel() {
return userProviderCache.getDelegate().getUserById(realm, cached.getId());
}
}

View file

@ -19,7 +19,9 @@ package org.keycloak.models.cache.infinispan;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
import org.keycloak.common.constants.ServiceAccountConstants;
@ -546,6 +548,10 @@ public class UserCacheSession implements UserCache.Streams, OnCreateComponent, O
return getDelegate().getUsersStream(realm, includeServiceAccounts);
}
@Override
public CredentialValidationOutput getUserByCredential(RealmModel realm, CredentialInput input) {
return getDelegate().getUserByCredential(realm, input);
}
@Override
public int getUsersCount(RealmModel realm, boolean includeServiceAccount) {
return getDelegate().getUsersCount(realm, includeServiceAccount);

View file

@ -18,6 +18,7 @@
package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@ -27,6 +28,8 @@ import org.keycloak.models.cache.infinispan.LazyLoader;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -50,6 +53,7 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
private final LazyLoader<UserModel, MultivaluedHashMap<String, String>> attributes;
private final LazyLoader<UserModel, Set<String>> roleMappings;
private final LazyLoader<UserModel, Set<String>> groups;
private final LazyLoader<UserModel, List<CredentialModel>> storedCredentials;
public CachedUser(Long revision, RealmModel realm, UserModel user, int notBefore) {
super(revision, user.getId());
@ -66,6 +70,7 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
this.attributes = new DefaultLazyLoader<>(userModel -> new MultivaluedHashMap<>(userModel.getAttributes()), MultivaluedHashMap::new);
this.roleMappings = new DefaultLazyLoader<>(userModel -> userModel.getRoleMappingsStream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet);
this.groups = new DefaultLazyLoader<>(userModel -> userModel.getGroupsStream().map(GroupModel::getId).collect(Collectors.toCollection(LinkedHashSet::new)), LinkedHashSet::new);
this.storedCredentials = new DefaultLazyLoader<>(userModel -> userModel.getUserCredentialManager().getStoredCredentialsStream().collect(Collectors.toCollection(LinkedList::new)), LinkedList::new);
}
public String getRealm() {
@ -119,4 +124,10 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm
public int getNotBefore() {
return notBefore;
}
public List<CredentialModel> getStoredCredentials(Supplier<UserModel> userModel) {
// clone the credential model before returning it, so that modifications don't pollute the cache
return storedCredentials.get(userModel).stream().map(CredentialModel::shallowClone).collect(Collectors.toList());
}
}

View file

@ -19,11 +19,13 @@ package org.keycloak.models.jpa;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.credential.LegacySingleUserCredentialManager;
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.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserEntity;
@ -34,6 +36,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
@ -46,7 +49,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import javax.persistence.LockModeType;
import static org.keycloak.utils.StreamsUtil.closing;
@ -515,6 +517,11 @@ public class UserAdapter implements UserModel.Streams, JpaModel<UserEntity> {
user.setServiceAccountClientLink(clientInternalId);
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
@Override
public boolean equals(Object o) {

View file

@ -1,247 +0,0 @@
/*
* 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("StorageProvideFactory (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);
}
}

View file

@ -30,10 +30,16 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.component.ComponentFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.CredentialProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
@ -52,6 +58,7 @@ import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.ComponentUtil;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.storage.client.ClientStorageProvider;
import org.keycloak.storage.datastore.LegacyDatastoreProvider;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.storage.managers.UserStorageSyncManager;
import org.keycloak.storage.user.ImportedUserValidation;
@ -76,7 +83,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
protected UserProvider localStorage() {
return session.userLocalStorage();
return ((LegacyDatastoreProvider) session.getProvider(DatastoreProvider.class)).userLocalStorage();
}
private UserFederatedStorageProvider getFederatedStorage() {
@ -122,6 +129,26 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
}
private static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
.map(f -> (T) session.getProvider(CredentialProvider.class, f.getId()));
}
@Override
public CredentialValidationOutput getUserByCredential(RealmModel realm, CredentialInput input) {
Stream<CredentialAuthentication> credentialAuthenticationStream = getEnabledStorageProviders(realm, CredentialAuthentication.class);
credentialAuthenticationStream = Stream.concat(credentialAuthenticationStream,
getCredentialProviders(session, CredentialAuthentication.class));
return credentialAuthenticationStream
.filter(credentialAuthentication -> credentialAuthentication.supportsCredentialAuthenticationFor(input.getType()))
.map(credentialAuthentication -> credentialAuthentication.authenticate(realm, input))
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
protected void deleteInvalidUser(final RealmModel realm, final UserModel user) {
String userId = user.getId();
String userName = user.getUsername();

View file

@ -18,11 +18,13 @@ package org.keycloak.storage.adapter;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.credential.LegacySingleUserCredentialManager;
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.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModelDefaultMethods;
import org.keycloak.models.utils.KeycloakModelUtils;
@ -254,6 +256,11 @@ public class InMemoryUserAdapter extends UserModelDefaultMethods.Streams {
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
@Override
public Stream<RoleModel> getRealmRoleMappingsStream() {
return getRoleMappingsStream().filter(RoleUtils::isRealmRole);

View file

@ -18,6 +18,7 @@ import org.keycloak.storage.LegacyStoreManagers;
import org.keycloak.storage.MigrationManager;
import org.keycloak.storage.RoleStorageManager;
import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
public class LegacyDatastoreProvider implements DatastoreProvider, LegacyStoreManagers {
private final LegacyDatastoreProviderFactory factory;
@ -35,6 +36,7 @@ public class LegacyDatastoreProvider implements DatastoreProvider, LegacyStoreMa
private GroupStorageManager groupStorageManager;
private ClientStorageManager clientStorageManager;
private UserProvider userStorageManager;
private UserFederatedStorageProvider userFederatedStorageProvider;
public LegacyDatastoreProvider(LegacyDatastoreProviderFactory factory, KeycloakSession session) {
this.factory = factory;
@ -80,6 +82,19 @@ public class LegacyDatastoreProvider implements DatastoreProvider, LegacyStoreMa
return userStorageManager;
}
@Override
public UserProvider userLocalStorage() {
return session.getProvider(UserProvider.class);
}
@Override
public UserFederatedStorageProvider userFederatedStorage() {
if (userFederatedStorageProvider == null) {
userFederatedStorageProvider = session.getProvider(UserFederatedStorageProvider.class);
}
return userFederatedStorageProvider;
}
private ClientProvider getClientProvider() {
// TODO: Extract ClientProvider from CacheRealmProvider and use that instead
ClientProvider cache = session.getProvider(CacheRealmProvider.class);
@ -192,6 +207,7 @@ public class LegacyDatastoreProvider implements DatastoreProvider, LegacyStoreMa
}
@Override
@Deprecated
public MigrationManager getMigrationManager() {
return new LegacyMigrationManager(session);
}

View file

@ -0,0 +1,283 @@
/*
* 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.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.storage.AbstractStorageManager;
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.
*
* This serves as a wrapper to specific strategies. The wrapping code implements the logic for {@link CredentialInputUpdater}s
* and {@link CredentialInputValidator}s. Storage specific strategies can be added like for example in
* org.keycloak.models.map.credential.MapSingleUserCredentialManagerStrategy.
*
* I tried to extract the federation specific parts to the {@link LegacySingleUserCredentialManagerStrategy} but the control
* flow in the existing logic: if <code>model == null || !model.isEnabled()</code>, the code will directly return, while
* the behavior of the strategy is to continue if it returns false and it will then try other providers.
*
* @author Alexander Schwartz
*/
public class LegacySingleUserCredentialManager extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel> implements SingleUserCredentialManager {
private final UserModel user;
private final KeycloakSession session;
private final RealmModel realm;
private final LegacySingleUserCredentialManagerStrategy strategy;
public LegacySingleUserCredentialManager(KeycloakSession session, RealmModel realm, UserModel user) {
super(session, UserStorageProviderFactory.class, UserStorageProvider.class, UserStorageProviderModel::new, "user");
this.user = user;
this.session = session;
this.realm = realm;
this.strategy = new LegacySingleUserCredentialManagerStrategy(session, realm, user);
}
@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);
}
}
strategy.validateCredentials(toValidate);
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 strategy.updateCredential(input) ||
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);
strategy.updateStoredCredential(cred);
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
throwExceptionIfInvalidUser(user);
return strategy.createStoredCredential(cred);
}
@Override
public boolean removeStoredCredentialById(String id) {
throwExceptionIfInvalidUser(user);
return strategy.removeStoredCredentialById(id);
}
@Override
public CredentialModel getStoredCredentialById(String id) {
return strategy.getStoredCredentialById(id);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
return strategy.getStoredCredentialsStream();
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return strategy.getStoredCredentialsByTypeStream(type);
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
return strategy.getStoredCredentialByNameAndType(name, type);
}
@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
throwExceptionIfInvalidUser(user);
return strategy.moveStoredCredentialTo(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(UserModel user) {
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");
}
}
}

View file

@ -0,0 +1,108 @@
/*
* 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.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.AbstractStorageManager;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.LegacyStoreManagers;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.util.List;
import java.util.stream.Stream;
/**
* Strategy for {@link LegacySingleUserCredentialManager} to handle classic local storage including federation.
*
* @author Alexander Schwartz
*/
public class LegacySingleUserCredentialManagerStrategy extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel> implements SingleUserCredentialManagerStrategy {
private final UserModel user;
private final RealmModel realm;
public LegacySingleUserCredentialManagerStrategy(KeycloakSession session, RealmModel realm, UserModel user) {
super(session, UserStorageProviderFactory.class, UserStorageProvider.class, UserStorageProviderModel::new, "user");
this.user = user;
this.realm = realm;
}
@Override
public void validateCredentials(List<CredentialInput> toValidate) {
}
@Override
public boolean updateCredential(CredentialInput input) {
return false;
}
@Override
public void updateStoredCredential(CredentialModel cred) {
getStoreForUser(user).updateCredential(realm, user, cred);
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
return getStoreForUser(user).createCredential(realm, user, cred);
}
@Override
public Boolean removeStoredCredentialById(String id) {
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) {
return getStoreForUser(user).moveCredentialTo(realm, user, id, newPreviousCredentialId);
}
private UserCredentialStore getStoreForUser(UserModel user) {
LegacyStoreManagers p = (LegacyStoreManagers) session.getProvider(DatastoreProvider.class);
if (StorageId.isLocalStorage(user.getId())) {
return (UserCredentialStore) p.userLocalStorage();
} else {
return (UserCredentialStore) p.userFederatedStorage();
}
}
}

View file

@ -5,6 +5,7 @@ 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 LegacyStoreManagers {
@ -17,4 +18,8 @@ public interface LegacyStoreManagers {
GroupProvider groupStorageManager();
UserProvider userStorageManager();
UserProvider userLocalStorage();
UserFederatedStorageProvider userFederatedStorage();
}

View file

@ -0,0 +1,39 @@
/*
* 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.map.credential;
import org.keycloak.credential.CredentialInput;
import java.util.List;
/**
* Standard implementation for a {@link MapSingleUserCredentialManagerEntity} where the store doesn't provide
* validation of credentials.
*
* @author Alexander Schwartz
*/
public class DefaultMapSingleUserCredentialManagerEntity implements MapSingleUserCredentialManagerEntity {
@Override
public void validateCredentials(List<CredentialInput> inputs) {
}
@Override
public boolean updateCredential(CredentialInput input) {
return false;
}
}

View file

@ -0,0 +1,217 @@
/*
* 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.map.credential;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.CredentialProviderFactory;
import org.keycloak.credential.SingleUserCredentialManagerStrategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.user.MapUserEntity;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Handling credentials for a given user.
*
* This serves as a wrapper to specific strategies. The wrapping code implements the logic for {@link CredentialInputUpdater}s
* and {@link CredentialInputValidator}s. Storage specific strategies can be added like for example in
* org.keycloak.models.map.credential.MapSingleUserCredentialManagerStrategy.
*
* @author Alexander Schwartz
*/
public class MapSingleUserCredentialManager implements SingleUserCredentialManager {
private final UserModel user;
private final KeycloakSession session;
private final RealmModel realm;
private final SingleUserCredentialManagerStrategy strategy;
public MapSingleUserCredentialManager(KeycloakSession session, RealmModel realm, UserModel user, MapUserEntity entity) {
this.user = user;
this.session = session;
this.realm = realm;
this.strategy = new MapSingleUserCredentialManagerStrategy(entity);
}
@Override
public boolean isValid(List<CredentialInput> inputs) {
if (!isValid(user)) {
return false;
}
List<CredentialInput> toValidate = new LinkedList<>(inputs);
strategy.validateCredentials(toValidate);
getCredentialProviders(session, CredentialInputValidator.class)
.forEach(validator -> validate(realm, user, toValidate, validator));
return toValidate.isEmpty();
}
@Override
public boolean updateCredential(CredentialInput input) {
return strategy.updateCredential(input) ||
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);
strategy.updateStoredCredential(cred);
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
throwExceptionIfInvalidUser(user);
return strategy.createStoredCredential(cred);
}
@Override
public boolean removeStoredCredentialById(String id) {
throwExceptionIfInvalidUser(user);
return strategy.removeStoredCredentialById(id);
}
@Override
public CredentialModel getStoredCredentialById(String id) {
return strategy.getStoredCredentialById(id);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
return strategy.getStoredCredentialsStream();
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return strategy.getStoredCredentialsByTypeStream(type);
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
return strategy.getStoredCredentialByNameAndType(name, type);
}
@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
throwExceptionIfInvalidUser(user);
return strategy.moveStoredCredentialTo(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) {
getCredentialProviders(session, CredentialInputUpdater.class)
.filter(updater -> updater.supportsCredentialType(credentialType))
.forEach(updater -> updater.disableCredentialType(realm, user, credentialType));
}
@Override
public Stream<String> getDisableableCredentialTypesStream() {
// TODO: ask the store
return getCredentialProviders(session, CredentialInputUpdater.class)
.flatMap(updater -> updater.getDisableableCredentialTypesStream(realm, user));
}
@Override
public boolean isConfiguredFor(String type) {
// TODO: ask the store
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(UserModel user) {
// TODO ask the store
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) {
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");
}
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.map.credential;
import org.keycloak.credential.CredentialInput;
import java.util.List;
/**
* Interface for credential management in entities in the map storage.
*
* @author Alexander Schwartz
*/
public interface MapSingleUserCredentialManagerEntity {
/**
* Validate the credentials of a user.
* Will remove all inputs from the list that have been successfully validated, all remaining entries
* weren't validated. An empty list signals to the caller that authentication has completed successfully.
*
* @param inputs Credential inputs as provided by a user
*/
void validateCredentials(List<CredentialInput> inputs);
/**
* Update the credentials for a user with the input provided by the user for this store.
* @param input new credentials as provided by the user
* @return true if the credential has been updated successfully, false otherwise. False might indicate that the
* credential type isn't supported of the new credentials aren't valid.
*/
boolean updateCredential(CredentialInput input);
}

View file

@ -0,0 +1,113 @@
/*
* 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.map.credential;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.SingleUserCredentialManagerStrategy;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserEntity;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Implementation of {@link SingleUserCredentialManagerStrategy} for map storages.
* Will delegate calls to the credential manager in the entity.
*
* @author Alexander Schwartz
*/
public class MapSingleUserCredentialManagerStrategy implements SingleUserCredentialManagerStrategy {
private final MapUserEntity entity;
public MapSingleUserCredentialManagerStrategy(MapUserEntity entity) {
this.entity = entity;
}
@Override
public void validateCredentials(List<CredentialInput> toValidate) {
entity.getUserCredentialManager().validateCredentials(toValidate);
}
@Override
public boolean updateCredential(CredentialInput input) {
return entity.getUserCredentialManager().updateCredential(input);
}
@Override
public void updateStoredCredential(CredentialModel credentialModel) {
entity.getCredential(credentialModel.getId()).ifPresent(c -> {
c.setCreatedDate(credentialModel.getCreatedDate());
c.setUserLabel(credentialModel.getUserLabel());
c.setType(credentialModel.getType());
c.setSecretData(credentialModel.getSecretData());
c.setCredentialData(credentialModel.getCredentialData());
});
}
@Override
public CredentialModel createStoredCredential(CredentialModel cred) {
MapUserCredentialEntity credentialEntity = MapUserCredentialEntity.fromModel(cred);
if (entity.getCredential(cred.getId()).isPresent()) {
throw new ModelDuplicateException("A CredentialModel with given id already exists");
}
entity.addCredential(credentialEntity);
return MapUserCredentialEntity.toModel(credentialEntity);
}
@Override
public Boolean removeStoredCredentialById(String id) {
return entity.removeCredential(id);
}
@Override
public CredentialModel getStoredCredentialById(String id) {
return entity.getCredential(id).map(MapUserCredentialEntity::toModel).orElse(null);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream() {
return Optional.ofNullable(entity.getCredentials()).orElse(Collections.emptyList()).stream()
.map(MapUserCredentialEntity::toModel);
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
return getStoredCredentialsStream()
.filter(credential -> Objects.equals(type, credential.getType()));
}
@Override
public CredentialModel getStoredCredentialByNameAndType(String name, String type) {
return getStoredCredentialsStream()
.filter(credential -> Objects.equals(name, credential.getUserLabel()))
.findFirst().orElse(null);
}
@Override
public boolean moveStoredCredentialTo(String id, String newPreviousCredentialId) {
return entity.moveCredential(id, newPreviousCredentialId);
}
}

View file

@ -0,0 +1,24 @@
package org.keycloak.models.map.storage;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.RealmModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.user.MapCredentialValidationOutput;
/**
* A map store transaction that can authenticate the credentials provided by a user.
*
* @author Alexander Schwartz
*/
public interface MapKeycloakTransactionWithAuth<V extends AbstractEntity, M> extends MapKeycloakTransaction<V, M> {
/**
* Authenticate a user with the provided input credentials. Use this, for example, for Kerberos SPNEGO
* authentication, where the user will be determined at the end of the interaction with the client.
* @param realm realm against which to authenticate against
* @param input information provided by the user
* @return Information on how to continue the conversion with the client, or a terminal result. For a successful
* authentication, will also contain information about the user.
*/
MapCredentialValidationOutput<V> authenticate(RealmModel realm, CredentialInput input);
}

View file

@ -0,0 +1,27 @@
package org.keycloak.models.map.storage;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.UpdatableEntity;
/**
* Implementing this interface signals that the store can validate credentials.
* This will be implemented, for example, by a store that supports SPNEGO for Kerberos authentication.
*
* @author Alexander Schwartz
*/
public interface MapStorageWithAuth<V extends AbstractEntity & UpdatableEntity, M> extends MapStorage<V, M> {
/**
* Determine which credential types a store supports.
* This method should be a cheap way to query the store before creating a more expensive transaction and performing an authentication.
*
* @param type supported credential type by this store, for example {@link CredentialModel#KERBEROS}.
* @return <code>true</code> if the credential type is supported by this storage
*/
boolean supportsCredentialType(String type);
@Override
MapKeycloakTransactionWithAuth<V, M> createTransaction(KeycloakSession session);
}

View file

@ -0,0 +1,62 @@
/*
* 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.map.user;
import org.keycloak.models.CredentialValidationOutput;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Output of a credential validation.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MapCredentialValidationOutput<V> {
private final V authenticatedUser;
private final CredentialValidationOutput.Status authStatus; // status whether user is authenticated or more steps needed
private final Map<String, String> state; // Additional state related to authentication. It can contain data to be sent back to client or data about used credentials.
public MapCredentialValidationOutput(V authenticatedUser, CredentialValidationOutput.Status authStatus, Map<String, String> state) {
this.authenticatedUser = authenticatedUser;
this.authStatus = authStatus;
this.state = state;
}
public static MapCredentialValidationOutput<?> failed() {
return new MapCredentialValidationOutput<Void>(null, CredentialValidationOutput.Status.FAILED, Collections.emptyMap());
}
public V getAuthenticatedUser() {
return authenticatedUser;
}
public CredentialValidationOutput.Status getAuthStatus() {
return authStatus;
}
/**
* State that is passed back by provider
*/
public Map<String, String> getState() {
return state;
}
}

View file

@ -24,11 +24,12 @@ import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.EntityWithAttributes;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.credential.DefaultMapSingleUserCredentialManagerEntity;
import org.keycloak.models.map.credential.MapSingleUserCredentialManagerEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@ -39,7 +40,7 @@ import java.util.Set;
@DeepCloner.Root
public interface MapUserEntity extends UpdatableEntity, AbstractEntity, EntityWithAttributes {
public abstract class AbstractUserEntity extends UpdatableEntity.Impl implements MapUserEntity {
abstract class AbstractUserEntity extends Impl implements MapUserEntity {
private static final Logger LOG = Logger.getLogger(MapUserProvider.class);
private String id;
@ -246,4 +247,9 @@ public interface MapUserEntity extends UpdatableEntity, AbstractEntity, EntityWi
Long getNotBefore();
void setNotBefore(Long notBefore);
@IgnoreForEntityImplementationGenerator
default MapSingleUserCredentialManagerEntity getUserCredentialManager() {
return new DefaultMapSingleUserCredentialManagerEntity();
}
}

View file

@ -22,11 +22,15 @@ import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.common.util.Time;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.UserCredentialStore;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.CredentialProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
@ -37,11 +41,14 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.SearchableFields;
import org.keycloak.models.UserProvider;
import org.keycloak.models.map.common.TimeAdapter;
import org.keycloak.models.map.credential.MapSingleUserCredentialManager;
import org.keycloak.models.map.storage.MapKeycloakTransactionWithAuth;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
@ -77,7 +84,7 @@ import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING;
import static org.keycloak.models.map.storage.QueryParameters.withCriteria;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
public class MapUserProvider implements UserProvider.Streams, UserCredentialStore.Streams {
public class MapUserProvider implements UserProvider.Streams {
private static final Logger LOG = Logger.getLogger(MapUserProvider.class);
private final KeycloakSession session;
@ -101,6 +108,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
public boolean checkUsernameUniqueness(RealmModel realm, String username) {
return getUserByUsername(realm, username) != null;
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new MapSingleUserCredentialManager(session, realm, this, entity);
}
};
}
@ -109,7 +121,7 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
return c -> false;
}
String realmId = realm.getId();
return entity -> Objects.equals(realmId, entity.getRealmId());
return entity -> entity.getRealmId() == null || Objects.equals(realmId, entity.getRealmId());
}
private ModelException userDoesntExistException() {
@ -747,94 +759,41 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
.map(entityToAdapterFunc(realm));
}
@Override
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
getEntityById(realm, user.getId())
.ifPresent(updateCredential(cred));
}
private Consumer<MapUserEntity> updateCredential(CredentialModel credentialModel) {
return user -> user.getCredential(credentialModel.getId()).ifPresent(c -> {
c.setCreatedDate(credentialModel.getCreatedDate());
c.setUserLabel(credentialModel.getUserLabel());
c.setType(credentialModel.getType());
c.setSecretData(credentialModel.getSecretData());
c.setCredentialData(credentialModel.getCredentialData());
});
}
@Override
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
LOG.tracef("createCredential(%s, %s, %s)%s", realm, user.getId(), cred.getId(), getShortStackTrace());
MapUserEntity userEntity = getEntityByIdOrThrow(realm, user.getId());
MapUserCredentialEntity credentialEntity = MapUserCredentialEntity.fromModel(cred);
if (userEntity.getCredential(cred.getId()).isPresent()) {
throw new ModelDuplicateException("A CredentialModel with given id already exists");
}
userEntity.addCredential(credentialEntity);
return MapUserCredentialEntity.toModel(credentialEntity);
}
@Override
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
LOG.tracef("removeStoredCredential(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace());
Optional<MapUserEntity> entityById = getEntityById(realm, user.getId());
if (!entityById.isPresent()) return false;
Boolean result = entityById.get().removeCredential(id);
return result == null ? true : result; // TODO: make removeStoredCredential return Boolean so the caller can correctly handle "I don't know" null answer
}
@Override
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
LOG.tracef("getStoredCredentialById(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace());
return getEntityById(realm, user.getId())
.flatMap(mapUserEntity -> mapUserEntity.getCredential(id))
.map(MapUserCredentialEntity::toModel)
.orElse(null);
}
@Override
public Stream<CredentialModel> getStoredCredentialsStream(RealmModel realm, UserModel user) {
LOG.tracef("getStoredCredentialsStream(%s, %s)%s", realm, user.getId(), getShortStackTrace());
return getEntityById(realm, user.getId())
.map(MapUserEntity::getCredentials)
.map(Collection::stream)
.orElseGet(Stream::empty)
.map(MapUserCredentialEntity::toModel);
}
@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(RealmModel realm, UserModel user, String type) {
LOG.tracef("getStoredCredentialsByTypeStream(%s, %s, %s)%s", realm, user.getId(), type, getShortStackTrace());
return getStoredCredentialsStream(realm, user)
.filter(credential -> Objects.equals(type, credential.getType()));
}
@Override
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
LOG.tracef("getStoredCredentialByNameAndType(%s, %s, %s, %s)%s", realm, user.getId(), name, type, getShortStackTrace());
return getStoredCredentialsByType(realm, user, type).stream()
.filter(credential -> Objects.equals(name, credential.getUserLabel()))
.findFirst().orElse(null);
}
@Override
public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) {
LOG.tracef("moveCredentialTo(%s, %s, %s, %s)%s", realm, user, id, newPreviousCredentialId, getShortStackTrace());
return getEntityByIdOrThrow(realm, user.getId()).moveCredential(id, newPreviousCredentialId);
}
@Override
public void close() {
}
public static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
.map(f -> (T) session.getProvider(CredentialProvider.class, f.getId()));
}
@Override
public CredentialValidationOutput getUserByCredential(RealmModel realm, CredentialInput input) {
// TODO: future implementations would narrow down the stream to those provider enabled for the specific realm
Stream<CredentialAuthentication> credentialAuthenticationStream = getCredentialProviders(session, CredentialAuthentication.class);
CredentialValidationOutput r = credentialAuthenticationStream
.filter(credentialAuthentication -> credentialAuthentication.supportsCredentialAuthenticationFor(input.getType()))
.map(credentialAuthentication -> credentialAuthentication.authenticate(realm, input))
.filter(Objects::nonNull)
.findFirst().orElse(null);
if (r == null && tx instanceof MapKeycloakTransactionWithAuth) {
MapCredentialValidationOutput<MapUserEntity> result = ((MapKeycloakTransactionWithAuth<MapUserEntity, UserModel>) tx).authenticate(realm, input);
if (result != null) {
UserModel user = null;
if (result.getAuthenticatedUser() != null) {
user = entityToAdapterFunc(realm).apply(result.getAuthenticatedUser());
}
r = new CredentialValidationOutput(user, result.getAuthStatus(), result.getState());
}
}
return r;
}
private DefaultModelCriteria<UserModel> addSearchToModelCriteria(String value,
DefaultModelCriteria<UserModel> mcb) {

View file

@ -8,21 +8,23 @@ import org.keycloak.models.RoleProvider;
import org.keycloak.models.UserProvider;
import org.keycloak.provider.Provider;
public interface DatastoreProvider extends Provider {
public ClientScopeProvider clientScopes();
ClientScopeProvider clientScopes();
public ClientProvider clients();
ClientProvider clients();
public GroupProvider groups();
GroupProvider groups();
public RealmProvider realms();
RealmProvider realms();
public RoleProvider roles();
RoleProvider roles();
public UserProvider users();
UserProvider users();
ExportImportManager getExportImportManager();
MigrationManager getMigrationManager();
}

View file

@ -0,0 +1,55 @@
/*
* 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 java.util.List;
import java.util.stream.Stream;
/**
* Use this to implement extendable strategies for the {@link org.keycloak.models.SingleUserCredentialManager}.
*/
public interface SingleUserCredentialManagerStrategy {
/**
* Validate the credentials passed as a list. The implementation should remove all credentials that validate
* successfully from the list. An empty list signals to the caller that authentication has completed successfully.
*/
void validateCredentials(List<CredentialInput> toValidate);
/**
* Update the credential.
* @return true is the credential was update, false otherwise
*/
boolean updateCredential(CredentialInput input);
void updateStoredCredential(CredentialModel cred);
CredentialModel createStoredCredential(CredentialModel cred) ;
Boolean removeStoredCredentialById(String id);
CredentialModel getStoredCredentialById(String id);
Stream<CredentialModel> getStoredCredentialsStream();
Stream<CredentialModel> getStoredCredentialsByTypeStream(String type);
CredentialModel getStoredCredentialByNameAndType(String name, String type);
boolean moveStoredCredentialTo(String id, String newPreviousCredentialId);
}

View file

@ -20,6 +20,7 @@ package org.keycloak.models;
import java.util.Comparator;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.storage.SearchableModelField;

View file

@ -0,0 +1,69 @@
/*
* 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.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import java.util.List;
import java.util.stream.Stream;
public interface SingleUserCredentialManager {
/**
* Validates list of credentials.
*/
boolean isValid(List<CredentialInput> inputs);
/**
* Updates a credentials of the user.
*/
boolean updateCredential(CredentialInput input);
void updateStoredCredential(CredentialModel cred);
CredentialModel createStoredCredential(CredentialModel cred);
boolean removeStoredCredentialById(String id);
CredentialModel getStoredCredentialById(String id);
Stream<CredentialModel> getStoredCredentialsStream();
Stream<CredentialModel> getStoredCredentialsByTypeStream(String type);
CredentialModel getStoredCredentialByNameAndType(String name, String type);
boolean moveStoredCredentialTo(String id, String newPreviousCredentialId);
void updateCredentialLabel(String credentialId, String userLabel);
void disableCredentialType(String credentialType);
Stream<String> getDisableableCredentialTypesStream();
boolean isConfiguredFor(String type);
// TODO: not needed for new store? -> no, will be removed without replacement
@Deprecated
boolean isConfiguredLocally(String type);
Stream<String> getConfiguredUserStorageCredentialTypesStream(UserModel user);
CredentialModel createCredentialThroughProvider(CredentialModel model);
}

View file

@ -18,8 +18,8 @@
package org.keycloak.models;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.storage.SearchableModelField;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
@ -118,7 +118,7 @@ public interface UserModel extends RoleMapperModel {
* Get timestamp of user creation. May be null for old users created before this feature introduction.
*/
Long getCreatedTimestamp();
void setCreatedTimestamp(Long timestamp);
boolean isEnabled();
@ -278,7 +278,7 @@ public interface UserModel extends RoleMapperModel {
default long getGroupsCount() {
return getGroupsCountByNameContaining(null);
}
default long getGroupsCountByNameContaining(String search) {
if (search == null) {
return getGroupsStream().count();
@ -298,6 +298,8 @@ public interface UserModel extends RoleMapperModel {
String getServiceAccountClientLink();
void setServiceAccountClientLink(String clientInternalId);
SingleUserCredentialManager getUserCredentialManager();
enum RequiredAction {
VERIFY_EMAIL,
UPDATE_PROFILE,

View file

@ -20,6 +20,7 @@ package org.keycloak.models.utils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import java.util.List;
@ -209,6 +210,11 @@ public class UserModelDelegate implements UserModel.Streams {
delegate.setServiceAccountClientLink(clientInternalId);
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return delegate.getUserCredentialManager();
}
public UserModel getDelegate() {
return delegate;
}
@ -259,4 +265,5 @@ public class UserModelDelegate implements UserModel.Streams {
public int hashCode() {
return getDelegate().getId().hashCode();
}
}

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.storage.user;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -59,6 +61,11 @@ public interface UserLookupProvider {
default UserModel getUserByUsername(RealmModel realm, String username) {
return getUserByUsername(username, realm);
}
default CredentialValidationOutput getUserByCredential(RealmModel realm, CredentialInput input) {
return null;
}
/**
* @deprecated Use {@link #getUserByUsername(RealmModel, String) getUserByUsername} instead.
*/

View file

@ -86,7 +86,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple
String spnegoToken = tokens[1];
UserCredentialModel spnegoCredential = UserCredentialModel.kerberos(spnegoToken);
CredentialValidationOutput output = context.getSession().userCredentialManager().authenticate(context.getSession(), context.getRealm(), spnegoCredential);
CredentialValidationOutput output = context.getSession().users().getUserByCredential(context.getRealm(), spnegoCredential);
if (output == null) {
logger.warn("Received kerberos token, but there is no user storage provider that handles kerberos credentials.");

View file

@ -19,16 +19,13 @@ package org.keycloak.credential;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.models.ModelException;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.OnUserCache;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError;
@ -41,9 +38,8 @@ import java.util.stream.Stream;
* @version $Revision: 1 $
*/
public class PasswordCredentialProvider implements CredentialProvider<PasswordCredentialModel>, CredentialInputUpdater.Streams,
CredentialInputValidator, OnUserCache {
CredentialInputValidator {
public static final String PASSWORD_CACHE_KEY = PasswordCredentialProvider.class.getName() + "." + PasswordCredentialModel.TYPE;
private static final Logger logger = Logger.getLogger(PasswordCredentialProvider.class);
protected final KeycloakSession session;
@ -57,18 +53,8 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
}
public PasswordCredentialModel getPassword(RealmModel realm, UserModel user) {
List<CredentialModel> passwords = null;
if (user instanceof CachedUserModel && !((CachedUserModel) user).isMarkedForEviction()) {
CachedUserModel cached = (CachedUserModel) user;
passwords = (List<CredentialModel>) cached.getCachedWith().get(PASSWORD_CACHE_KEY);
}
// if the model was marked for eviction while passwords were initialized, override it from credentialStore
if (!(user instanceof CachedUserModel) || ((CachedUserModel) user).isMarkedForEviction()) {
passwords = getCredentialStore().getStoredCredentialsByTypeStream(realm, user, getType()).collect(Collectors.toList());
}
if (passwords == null || passwords.isEmpty()) return null;
List<CredentialModel> passwords = getCredentialStore().getStoredCredentialsByTypeStream(realm, user, getType()).collect(Collectors.toList());
if (passwords.isEmpty()) return null;
return PasswordCredentialModel.createFromCredentialModel(passwords.get(0));
}
@ -123,10 +109,6 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
.collect(Collectors.toList())
.forEach(p -> getCredentialStore().removeStoredCredential(realm, user, p.getId()));
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
return createdCredential;
}
@ -150,59 +132,6 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
return hash;
}
/*@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) return false;
if (!(input instanceof UserCredentialModel)) {
logger.debug("Expected instance of UserCredentialModel for CredentialInput");
return false;
}
UserCredentialModel cred = (UserCredentialModel)input;
PasswordPolicy policy = realm.getPasswordPolicy();
PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, cred.getValue());
if (error != null) throw new ModelException(error.getMessage(), error.getParameters());
PasswordHashProvider hash = getHashProvider(policy);
if (hash == null) {
return false;
}
CredentialModel oldPassword = getPassword(realm, user);
expirePassword(realm, user, policy);
CredentialModel newPassword = new CredentialModel();
newPassword.setType(CredentialModel.PASSWORD);
long createdDate = Time.currentTimeMillis();
newPassword.setCreatedDate(createdDate);
hash.encode(cred.getValue(), policy.getHashIterations(), newPassword);
getCredentialStore().createCredential(realm, user, newPassword);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
return true;
}*/
/*@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) return;
PasswordPolicy policy = realm.getPasswordPolicy();
expirePassword(realm, user, policy);
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
if (!getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD).isEmpty()) {
Set<String> set = new HashSet<>();
set.add(CredentialModel.PASSWORD);
return set;
} else {
return Collections.EMPTY_SET;
}
}*/
@Override
public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(getType());
@ -241,7 +170,7 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
}
PasswordCredentialModel password = getPassword(realm, user);
if (password == null) {
logger.debugv("No password cached or stored for user {0} ", user.getUsername());
logger.debugv("No password stored for user {0} ", user.getUsername());
return false;
}
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, password.getPasswordCredentialData().getAlgorithm());
@ -271,21 +200,9 @@ public class PasswordCredentialProvider implements CredentialProvider<PasswordCr
newPassword.setUserLabel(password.getUserLabel());
getCredentialStore().updateCredential(realm, user, newPassword);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
return true;
}
@Override
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
List<CredentialModel> passwords = getCredentialStore().getStoredCredentialsByTypeStream(realm, user, getType())
.collect(Collectors.toList());
user.getCachedWith().put(PASSWORD_CACHE_KEY, passwords);
}
@Override
public String getType() {
return PasswordCredentialModel.TYPE;

View file

@ -24,15 +24,10 @@ import org.keycloak.models.UserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.OnUserCache;
import org.keycloak.models.cache.UserCache;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.storage.AbstractStorageManager;
import org.keycloak.storage.CacheableStorageProviderModel;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.StorageId;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@ -41,131 +36,88 @@ import java.util.stream.Stream;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserCredentialStoreManager extends AbstractStorageManager<Provider, CacheableStorageProviderModel>
public class UserCredentialStoreManager
implements UserCredentialManager.Streams, OnUserCache {
private final KeycloakSession session;
public UserCredentialStoreManager(KeycloakSession session) {
super(session, ProviderFactory.class, Provider.class, componentModel -> null, "user");
}
protected UserCredentialStore getStoreForUser(UserModel user) {
if (StorageId.isLocalStorage(user)) {
return (UserCredentialStore) session.userLocalStorage();
} else {
return (UserCredentialStore) session.userFederatedStorage();
}
this.session = session;
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
throwExceptionIfInvalidUser(user);
getStoreForUser(user).updateCredential(realm, user, cred);
user.getUserCredentialManager().updateStoredCredential(cred);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
throwExceptionIfInvalidUser(user);
return getStoreForUser(user).createCredential(realm, user, cred);
return user.getUserCredentialManager().createStoredCredential(cred);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
throwExceptionIfInvalidUser(user);
boolean removalResult = getStoreForUser(user).removeStoredCredential(realm, user, id);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
return removalResult;
return user.getUserCredentialManager().removeStoredCredentialById(id);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
return getStoreForUser(user).getStoredCredentialById(realm, user, id);
return user.getUserCredentialManager().getStoredCredentialById(id);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public Stream<CredentialModel> getStoredCredentialsStream(RealmModel realm, UserModel user) {
return getStoreForUser(user).getStoredCredentialsStream(realm, user);
return user.getUserCredentialManager().getStoredCredentialsStream();
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public Stream<CredentialModel> getStoredCredentialsByTypeStream(RealmModel realm, UserModel user, String type) {
return getStoreForUser(user).getStoredCredentialsByTypeStream(realm, user, type);
return user.getUserCredentialManager().getStoredCredentialsByTypeStream(type);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
return getStoreForUser(user).getStoredCredentialByNameAndType(realm, user, name, type);
return user.getUserCredentialManager().getStoredCredentialByNameAndType(name, type);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId){
throwExceptionIfInvalidUser(user);
return getStoreForUser(user).moveCredentialTo(realm, user, id, newPreviousCredentialId);
return user.getUserCredentialManager().moveStoredCredentialTo(id, newPreviousCredentialId);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean isValid(RealmModel realm, UserModel user, CredentialInput... inputs) {
return isValid(realm, user, Arrays.asList(inputs));
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public CredentialModel createCredentialThroughProvider(RealmModel realm, UserModel user, 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);
return user.getUserCredentialManager().createCredentialThroughProvider(model);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public void updateCredentialLabel(RealmModel realm, UserModel user, String credentialId, String userLabel){
throwExceptionIfInvalidUser(user);
CredentialModel credential = getStoredCredentialById(realm, user, credentialId);
credential.setUserLabel(userLabel);
getStoreForUser(user).updateCredential(realm, user, credential);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
user.getUserCredentialManager().updateCredentialLabel(credentialId, userLabel);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean isValid(RealmModel realm, UserModel user, List<CredentialInput> inputs) {
if (!isValid(user)) {
return false;
}
List<CredentialInput> toValidate = new LinkedList<>(inputs);
String providerId = StorageId.isLocalStorage(user) ? user.getFederationLink() : StorageId.resolveProviderId(user);
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);
}
*/
}
if (toValidate.isEmpty()) return true;
getCredentialProviders(session, CredentialInputValidator.class)
.forEach(validator -> validate(realm, user, toValidate, validator));
return toValidate.isEmpty();
}
private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate, CredentialInputValidator validator) {
toValidate.removeIf(input -> validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input));
return user.getUserCredentialManager().isValid(inputs);
}
@Deprecated // Keep this up to and including Keycloak 19, then inline
public static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
@ -173,134 +125,53 @@ public class UserCredentialStoreManager extends AbstractStorageManager<Provider,
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
String providerId = StorageId.isLocalStorage(user) ? user.getFederationLink() : StorageId.resolveProviderId(user);
if (!StorageId.isLocalStorage(user)) 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));
return user.getUserCredentialManager().updateCredential(input);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
String providerId = StorageId.isLocalStorage(user) ? user.getFederationLink() : StorageId.resolveProviderId(user);
if (!StorageId.isLocalStorage(user)) 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));
user.getUserCredentialManager().disableCredentialType(credentialType);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
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();
return user.getUserCredentialManager().getDisableableCredentialTypesStream();
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean isConfiguredFor(RealmModel realm, UserModel user, 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(realm, user, type);
}
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;
return user.getUserCredentialManager().isConfiguredFor(type);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public boolean isConfiguredLocally(RealmModel realm, UserModel user, String type) {
return getCredentialProviders(session, CredentialInputValidator.class)
.anyMatch(validator -> validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type));
// TODO: no longer used, can be removed
return user.getUserCredentialManager().isConfiguredLocally(type);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public CredentialValidationOutput authenticate(KeycloakSession session, RealmModel realm, CredentialInput input) {
Stream<CredentialAuthentication> credentialAuthenticationStream = getEnabledStorageProviders(realm, CredentialAuthentication.class);
credentialAuthenticationStream = Stream.concat(credentialAuthenticationStream,
getCredentialProviders(session, CredentialAuthentication.class));
return credentialAuthenticationStream
.filter(credentialAuthentication -> credentialAuthentication.supportsCredentialAuthenticationFor(input.getType()))
.map(credentialAuthentication -> credentialAuthentication.authenticate(realm, input))
.findFirst().orElse(null);
// TODO: no longer used, can be removed
return session.users().getUserByCredential(realm, input);
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, then remove it together with the OnUserCache class
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
getCredentialProviders(session, OnUserCache.class).forEach(validator -> validator.onCache(realm, user, delegate));
}
@Override
@Deprecated // Keep this up to and including Keycloak 19, the use methods on user.getUserCredentialManager() instead
public Stream<String> getConfiguredUserStorageCredentialTypesStream(RealmModel realm, UserModel user) {
return getCredentialProviders(session, CredentialProvider.class).map(CredentialProvider::getType)
.filter(credentialType -> UserStorageCredentialConfigured.CONFIGURED == isConfiguredThroughUserStorage(realm, user, credentialType));
return user.getUserCredentialManager().getConfiguredUserStorageCredentialTypesStream(user);
}
@Override
@ -308,14 +179,4 @@ public class UserCredentialStoreManager extends AbstractStorageManager<Provider,
}
private boolean isValid(UserModel user) {
return user != null && user.getServiceAccountClientLink() == null;
}
private void throwExceptionIfInvalidUser(UserModel user) {
if (user == null || isValid(user)) {
return;
}
throw new RuntimeException("You can not manage credentials for this user");
}
}

View file

@ -166,11 +166,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
}
@Override
@Deprecated // put deprecated on the interface
public UserProvider userLocalStorage() {
// TODO: if we would call users() here, we could get the cache in legacy mode instead and would then loop
return getProvider(UserProvider.class);
}
@Override
@Deprecated // put deprecated on the interface (all *LocalStorage, all *Managers)
public RealmProvider realmLocalStorage() {
return realms();
}

View file

@ -73,9 +73,9 @@ import org.keycloak.services.resources.account.AccountFormService;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.validation.Validation;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.userprofile.ValidationException;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.ValidationException;
import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.BadRequestException;
@ -97,7 +97,6 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
@ -219,7 +218,7 @@ public class UserResource {
errors.add(new ErrorRepresentation(error.getFormattedMessage(new AdminMessageFormatter(session, user))));
}
return ErrorResponse.errors(errors, Response.Status.BAD_REQUEST);
return ErrorResponse.errors(errors, Status.BAD_REQUEST);
}
return null;
@ -640,8 +639,8 @@ public class UserResource {
public Stream<CredentialRepresentation> credentials(){
auth.users().requireManage(user);
return session.userCredentialManager().getStoredCredentialsStream(realm, user)
.peek(model -> model.setSecretData(null))
.map(ModelToRepresentation::toRepresentation);
.map(ModelToRepresentation::toRepresentation)
.peek(credentialRepresentation -> credentialRepresentation.setSecretData(null));
}
@ -686,7 +685,7 @@ public class UserResource {
* Update a credential label for a user
*/
@PUT
@Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
@Path("credentials/{credentialId}/userLabel")
public void setCredentialUserLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
auth.users().requireManage(user);

View file

@ -28,3 +28,4 @@ org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelSpi
org.keycloak.protocol.oidc.grants.ciba.resolvers.CIBALoginUserResolverSpi
org.keycloak.protocol.oidc.rar.AuthorizationRequestParserSpi
org.keycloak.services.resources.admin.ext.AdminRealmResourceSpi
org.keycloak.credential.SingleUserCredentialManagerSpi

View file

@ -33,12 +33,14 @@ import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.UserCache;
@ -101,6 +103,10 @@ public class BackwardsCompatibilityUserStorage implements UserLookupProvider, Us
}
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
};
}

View file

@ -21,8 +21,10 @@ import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
@ -162,7 +164,11 @@ public class PassThroughFederatedUserStorageProvider implements
@Override
public void setUsername(String username) {
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
};
}

View file

@ -16,20 +16,23 @@
*/
package org.keycloak.testsuite.federation;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
@ -46,7 +49,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
import static org.keycloak.utils.StreamsUtil.paginatedStream;
@ -81,9 +84,9 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
String editModeString = model.getConfig().getFirst(LDAPConstants.EDIT_MODE);
if (editModeString == null) {
this.editMode = UserStorageProvider.EditMode.UNSYNCED;
this.editMode = EditMode.UNSYNCED;
} else {
this.editMode = UserStorageProvider.EditMode.valueOf(editModeString);
this.editMode = EditMode.valueOf(editModeString);
}
}
@ -140,6 +143,11 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
public String getFederationLink() {
return model.getId();
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
};
}
@ -153,7 +161,7 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (editMode == UserStorageProvider.EditMode.READ_ONLY) {
if (editMode == EditMode.READ_ONLY) {
throw new ReadOnlyException("Federated storage is not writable");
}
if (!(input instanceof UserCredentialModel)) {
@ -215,7 +223,7 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
@Override
public UserModel addUser(RealmModel realm, String username) {
if (editMode == UserStorageProvider.EditMode.READ_ONLY) {
if (editMode == EditMode.READ_ONLY) {
throw new ReadOnlyException("Federated storage is not writable");
}
@ -225,7 +233,7 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
if (editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED) {
if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) {
log.warnf("User '%s' can't be deleted in LDAP as editMode is '%s'. Deleting user just from Keycloak DB, but he will be re-imported from LDAP again once searched in Keycloak", user.getUsername(), editMode.toString());
userPasswords.remove(translateUserName(user.getUsername()));
return true;
@ -235,7 +243,7 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
}
public boolean removeUserByName(String userName) {
if (editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED) {
if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) {
log.warnf("User '%s' can't be deleted in LDAP as editMode is '%s'. Deleting user just from Keycloak DB, but he will be re-imported from LDAP again once searched in Keycloak", userName, editMode.toString());
userPasswords.remove(translateUserName(userName));
return true;

View file

@ -19,10 +19,12 @@ package org.keycloak.testsuite.federation;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.LegacySingleUserCredentialManager;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.SingleUserCredentialManager;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
@ -132,6 +134,11 @@ public class UserPropertyFileStorage implements UserLookupProvider.Streams, User
public void setUsername(String username) {
throw new RuntimeException("Unsupported");
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
};
} else {
return new AbstractUserAdapter.Streams(session, realm, model) {
@ -139,6 +146,11 @@ public class UserPropertyFileStorage implements UserLookupProvider.Streams, User
public String getUsername() {
return username;
}
@Override
public SingleUserCredentialManager getUserCredentialManager() {
return new LegacySingleUserCredentialManager(session, realm, this);
}
};
}
}