All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.arbeitsagentur.opdt.keycloak.cassandra.user.CassandraCredentialManager Maven / Gradle / Ivy

/*
 * Copyright 2022 IT-Systemhaus der Bundesagentur fuer Arbeit
 *
 * 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 de.arbeitsagentur.opdt.keycloak.cassandra.user;

import de.arbeitsagentur.opdt.keycloak.cassandra.user.persistence.UserRepository;
import de.arbeitsagentur.opdt.keycloak.cassandra.user.persistence.entities.CredentialValue;
import de.arbeitsagentur.opdt.keycloak.cassandra.user.persistence.entities.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.common.util.reflections.Types;
import org.keycloak.credential.*;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;

import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

@JBossLog
@RequiredArgsConstructor
public class CassandraCredentialManager implements SubjectCredentialManager {
    private static final int PRIORITY_DIFFERENCE = 10;
    private final KeycloakSession session;
    private final RealmModel realm;
    private final UserRepository userRepository;
    private final UserModel user;
    private final User userEntity;

    @Override
    public boolean isValid(List inputs) {
        if (!isValid(user)) {
            return false;
        }

        List toValidate = new LinkedList<>(inputs);

        getCredentialProviders(session, CredentialInputValidator.class)
            .forEach(validator -> validate(realm, user, toValidate, validator));

        return toValidate.isEmpty();
    }

    @Override
    public boolean updateCredential(CredentialInput input) {
        return getCredentialProviders(session, CredentialInputUpdater.class)
            .filter(updater -> updater.supportsCredentialType(input.getType()))
            .anyMatch(updater -> updater.updateCredential(realm, user, input));
    }

    @Override
    public void updateStoredCredential(CredentialModel cred) {
        throwExceptionIfInvalidUser(user);
        CredentialValue credential = fromModel(cred);
        userEntity.getCredentials().remove(credential);
        userEntity.getCredentials().add(credential);
        userRepository.insertOrUpdate(userEntity);
    }

    @Override
    public CredentialModel createStoredCredential(CredentialModel cred) {
        throwExceptionIfInvalidUser(user);
        boolean existsAlready = userEntity.hasCredential(cred.getId());

        if (existsAlready) {
            throw new ModelDuplicateException("A CredentialModel with given id already exists");
        }

        CredentialValue credential = fromModel(cred);

        List credentials = userEntity.getSortedCredentials();
        int priority = credentials.isEmpty() ? PRIORITY_DIFFERENCE : credentials.get(credentials.size() - 1).getPriority() + PRIORITY_DIFFERENCE;
        credential.setPriority(priority);

        userEntity.getCredentials().remove(credential);
        userEntity.getCredentials().add(credential);
        userRepository.insertOrUpdate(userEntity);

        return toModel(credential);
    }

    @Override
    public boolean removeStoredCredentialById(String id) {
        throwExceptionIfInvalidUser(user);

        boolean removed = userEntity.getCredentials().remove(CredentialValue.builder().id(id).build());

        if (!removed) {
            return false;
        }

        userRepository.insertOrUpdate(userEntity);
        return true;
    }

    @Override
    public CredentialModel getStoredCredentialById(String id) {
        CredentialValue credential = userEntity.getCredentials().stream()
            .filter(c -> c.getId().equals(id))
            .findFirst()
            .orElse(null);

        if (credential == null) {
            return null;
        }
        return toModel(credential);
    }

    @Override
    public Stream getStoredCredentialsStream() {
        return userEntity.getSortedCredentials().stream()
            .map(this::toModel);
    }

    @Override
    public Stream 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 credentialId, String newPreviousCredentialId) {
        throwExceptionIfInvalidUser(user);

        // 1 - Get all credentials from the entity.
        List credentialsList = userEntity.getSortedCredentials();

        // 2 - Find indexes of our and newPrevious credential
        int ourCredentialIndex = -1;
        int newPreviousCredentialIndex = -1;
        CredentialValue ourCredential = null;
        int i = 0;
        for (CredentialValue credential : credentialsList) {
            if (credentialId.equals(credential.getId())) {
                ourCredentialIndex = i;
                ourCredential = credential;
            } else if (newPreviousCredentialId != null && newPreviousCredentialId.equals(credential.getId())) {
                newPreviousCredentialIndex = i;
            }
            i++;
        }

        if (ourCredentialIndex == -1) {
            log.debugf("Not found credential with id [%s] of user [%s]", credentialId, user.getUsername());
            return false;
        }

        if (newPreviousCredentialId != null && newPreviousCredentialIndex == -1) {
            log.debugf("Can't move up credential with id [%s] of user [%s]", credentialId, user.getUsername());
            return false;
        }

        // 3 - Compute index where we move our credential
        int toMoveIndex = newPreviousCredentialId == null ? 0 : newPreviousCredentialIndex + 1;

        // 4 - Insert our credential to new position, remove it from the old position
        if (toMoveIndex == ourCredentialIndex) return true;
        credentialsList.add(toMoveIndex, ourCredential);
        int indexToRemove = toMoveIndex < ourCredentialIndex ? ourCredentialIndex + 1 : ourCredentialIndex;
        credentialsList.remove(indexToRemove);

        // 5 - newList contains credentials in requested order now. Iterate through whole list and change priorities accordingly.
        int expectedPriority = 0;
        for (CredentialValue credential : credentialsList) {
            expectedPriority += PRIORITY_DIFFERENCE;
            if (credential.getPriority() != expectedPriority) {
                credential.setPriority(expectedPriority);

                log.tracef("Priority of credential [%s] of user [%s] changed to [%d]", credential.getId(), user.getUsername(), expectedPriority);

                userEntity.getCredentials().remove(credential);
                userEntity.getCredentials().add(credential);
                userRepository.insertOrUpdate(userEntity);
            }
        }

        return true;
    }

    @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 getDisableableCredentialTypesStream() {
        return getCredentialProviders(session, CredentialInputUpdater.class)
            .flatMap(updater -> updater.getDisableableCredentialTypesStream(realm, user))
            .distinct();
    }

    @Override
    public boolean isConfiguredFor(String type) {
        return getCredentialProviders(session, CredentialInputValidator.class)
            .anyMatch(validator -> validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type));
    }

    @Override
    @Deprecated
    public boolean isConfiguredLocally(String type) {
        throw new IllegalArgumentException("this is not supported for map storage");
    }

    @Override
    @Deprecated
    public Stream getConfiguredUserStorageCredentialTypesStream() {
        // used in the old admin console for users to determine if a password is set for a user
        // not used in the new admin console
        return Stream.empty();
    }

    @Override
    @Deprecated
    public CredentialModel createCredentialThroughProvider(CredentialModel model) {
        // this is still called when importing/creating a user via RepresentationToModel.createCredentials
        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 boolean isValid(UserModel user) {
        Objects.requireNonNull(user);
        return user.getServiceAccountClientLink() == null;
    }

    private void validate(RealmModel realm, UserModel user, List toValidate, CredentialInputValidator validator) {
        toValidate.removeIf(input -> validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input));
    }

    private static  Stream getCredentialProviders(KeycloakSession session, Class type) {
        //noinspection unchecked
        return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
            .filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
            .map(f -> (T) session.getProvider(CredentialProvider.class, f.getId()));
    }

    private void throwExceptionIfInvalidUser(UserModel user) {
        if (!isValid(user)) {
            throw new RuntimeException("You can not manage credentials for this user");
        }
    }

    private CredentialValue fromModel(CredentialModel model) {
        return CredentialValue.builder()
            .id(model.getId() == null ? KeycloakModelUtils.generateId() : model.getId())
            .created(model.getCreatedDate())
            .userLabel(model.getUserLabel())
            .type(model.getType())
            .secretData(model.getSecretData())
            .credentialData(model.getCredentialData())
            .build();
    }

    private CredentialModel toModel(CredentialValue entity) {
        CredentialModel credentialModel = new CredentialModel();
        credentialModel.setId(entity.getId());
        credentialModel.setCreatedDate(entity.getCreated());
        credentialModel.setUserLabel(entity.getUserLabel());
        credentialModel.setType(entity.getType());
        credentialModel.setSecretData(entity.getSecretData());
        credentialModel.setCredentialData(entity.getCredentialData());

        return credentialModel;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy