SecretValueDeserializer.java

package net.morimekta.config.jackson;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import net.morimekta.config.SecretsManager;

import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.Objects;

/**
 * Deserializer meant to read secrets into string fields. The default behavior
 * is to just read secrets from environment variables. To read secrets from the
 * secrets manager, it has to be set in the deserialization context as an
 * attribute:
 *
 * <pre><code>{@code
 * objectManager
 *     .reader()
 *     .withAttribute(SecretsManager.class, secretsManager)
 *     .readValue(from, MyConfig.class)
 * }</code></pre>
 * <p>
 * Or use the feature of the <code>SecretManagerDeserializer</code> that it will
 * insert itself into the deserializer context when itself is deserialized.
 * Example for yaml:
 * <pre><code>{@code
 * ---
 * secretsManager:
 *   dir: "/my/secrets"
 * mySecret: "${SECRET_NAME}"
 * }</code></pre>
 */
public class SecretValueDeserializer extends StdDeserializer<String> {
    public SecretValueDeserializer() {
        super(String.class);
    }

    @Override
    public String deserialize(
            JsonParser jsonParser,
            DeserializationContext deserializationContext) throws IOException {
        if (Objects.requireNonNull(jsonParser.currentToken()) == JsonToken.VALUE_STRING) {
            var value = jsonParser.getValueAsString();
            try {
                if (value.startsWith("${") && value.endsWith("}")) {
                    var manager = (SecretsManager) deserializationContext.getAttribute(SecretsManager.class);
                    var secretName = value.substring(2, value.length() - 1).trim();
                    if (manager != null) {
                        if (manager.exists(secretName)) {
                            return manager.get(secretName).getAsString();
                        }
                    }

                    var envValue = System.getenv(secretName);
                    if (envValue == null || envValue.isEmpty()) {
                        throw new JsonParseException(
                                jsonParser,
                                "Unknown secret '" + secretName + "'",
                                jsonParser.currentTokenLocation());
                    }
                    return envValue;
                }
            } catch (IllegalArgumentException | IllegalStateException e) {
                throw new JsonParseException(
                        jsonParser,
                        e.getMessage(),
                        jsonParser.currentTokenLocation(),
                        e);
            } catch (NoSuchElementException e) {
                throw new JsonParseException(
                        jsonParser,
                        "Unknown secret " + value,
                        jsonParser.currentTokenLocation(),
                        e);
            }
        }
        // Fall back to standard string parsing.
        return this._parseString(jsonParser, deserializationContext, this);
    }
}