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

io.gravitee.node.secrets.service.conf.GraviteeConfigurationSecretResolverDispatcher Maven / Gradle / Ivy

package io.gravitee.node.secrets.service.conf;

import io.gravitee.common.util.EnvironmentUtils;
import io.gravitee.node.api.secrets.SecretManagerConfiguration;
import io.gravitee.node.api.secrets.SecretProvider;
import io.gravitee.node.api.secrets.errors.SecretManagerConfigurationException;
import io.gravitee.node.api.secrets.errors.SecretManagerException;
import io.gravitee.node.api.secrets.errors.SecretProviderNotFoundException;
import io.gravitee.node.api.secrets.model.*;
import io.gravitee.node.api.secrets.util.ConfigHelper;
import io.gravitee.node.secrets.plugins.SecretProviderPluginManager;
import io.gravitee.node.secrets.service.AbstractSecretProviderDispatcher;
import io.reactivex.rxjava3.core.Maybe;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;

/**
 * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
 * @author GraviteeSource Team
 */
@Slf4j
public class GraviteeConfigurationSecretResolverDispatcher extends AbstractSecretProviderDispatcher {

    private static final String SECRETS_CONFIG_KEY = "secrets";
    private final Environment environment;

    private final Map secrets = Collections.synchronizedMap(new HashMap<>());

    @Getter
    @Accessors(fluent = true)
    private final List enabledProviders = new ArrayList<>();

    public GraviteeConfigurationSecretResolverDispatcher(SecretProviderPluginManager secretProviderPluginManager, Environment environment) {
        super(secretProviderPluginManager);
        this.environment = environment;
        setupConverters((ConfigurableEnvironment) environment);
        secretProviderPluginManager.setOnNewPluginCallback(pluginId -> {
            if (isEnabled(pluginId)) {
                super.createAndRegister(pluginId);
                enabledProviders.add(pluginId);
            }
        });
    }

    @SuppressWarnings("java:S1604")
    private void setupConverters(ConfigurableEnvironment environment) {
        // can't use lambdas here or Spring complains because it cannot infer types
        environment
            .getConversionService()
            .addConverter(
                new Converter() {
                    @Override
                    public String convert(@Nonnull Secret source) {
                        return source.asString();
                    }
                }
            );
        environment
            .getConversionService()
            .addConverter(
                new Converter() {
                    @Override
                    public byte[] convert(@Nonnull Secret source) {
                        return source.asBytes();
                    }
                }
            );
    }

    @Override
    public boolean isEnabled(String pluginId) {
        return environment.getProperty(String.format("%s.%s.enabled", SECRETS_CONFIG_KEY, pluginId), boolean.class, false);
    }

    @Override
    public  T readConfiguration(String pluginId, Class configurationClass) {
        Map configurationProperties = ConfigHelper.removePrefix(
            EnvironmentUtils.getAllProperties((ConfigurableEnvironment) environment),
            "%s.%s".formatted(SECRETS_CONFIG_KEY, pluginId)
        );

        try {
            @SuppressWarnings("unchecked")
            Constructor constructor = (Constructor) configurationClass.getDeclaredConstructor(Map.class);
            return constructor.newInstance(configurationProperties);
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new SecretManagerConfigurationException(
                "Could not create configuration class for secret manager: %s".formatted(pluginId),
                e
            );
        }
    }

    @Override
    public Maybe resolve(SecretMount secretMount) throws SecretProviderNotFoundException, SecretManagerException {
        if (secrets.containsKey(secretMount.location())) {
            return Maybe.just(secrets.get(secretMount.location()));
        }
        return super.resolve(secretMount).doOnSuccess(secretMap -> secrets.put(secretMount.location(), secretMap));
    }

    /**
     * Check if the value given can be handled by a provider.
     *
     * @param location the URL of a secret
     * @return true if there is a provider able to handle this URL
     */
    public boolean canHandle(String location) {
        Objects.requireNonNull(location);
        return (
            location.startsWith(SecretProvider.PLUGIN_URL_SCHEME) &&
            enabledProviders().stream().anyMatch(manager -> canProviderHandle(location, manager))
        );
    }

    /**
     * Check if the value given can be handled by a provider and if the URL can to be use to resolve a single value
     *
     * @param location the URL of a secret
     * @return true if they location can return a single secret
     */
    public boolean canResolveSingleValue(String location) {
        Objects.requireNonNull(location);
        if (canHandle(location)) {
            try {
                SecretMount secretMount = toSecretMount(location);
                if (secretMount.isKeyEmpty()) {
                    throw new IllegalArgumentException(
                        "Secret URL should must specify a 'key' in order to resolve a single value, such as: %s:".formatted(location)
                    );
                }
                return true;
            } catch (IllegalArgumentException | SecretProviderNotFoundException e) {
                // URL might not be suitable for resolving property
                return false;
            }
        }
        return false;
    }

    /**
     * Uses a secret provider to convert a URL to {@link SecretMount}
     *
     * @param location secret location
     * @return a {@link SecretMount}
     * @throws SecretProviderNotFoundException     if the URL points a non-existing secret provider
     * @throws SecretManagerConfigurationException if the URL processing led to an error
     * @throws IllegalArgumentException            if the URL is well formatted
     */
    public SecretMount toSecretMount(String location) {
        SecretURL url = SecretURL.from(location);
        return this.findSecretProvider(url.provider())
            .map(secretProvider -> {
                try {
                    return secretProvider.fromURL(url);
                } catch (IllegalArgumentException e) {
                    throw new SecretManagerConfigurationException("cannot create secret URL from: " + location, e);
                }
            })
            .orElseThrow(() ->
                new SecretProviderNotFoundException(
                    AbstractSecretProviderDispatcher.SECRET_PROVIDER_NOT_FOUND_FOR_ID.formatted(url.provider())
                )
            );
    }

    // for tests
    Map secrets() {
        return Map.copyOf(secrets);
    }

    private static boolean canProviderHandle(String location, String manager) {
        return location.startsWith("%s%s/".formatted(SecretURL.SCHEME, manager));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy