SecretValueDeserializer.java
/*
* Copyright 2024 Morimekta Utils Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 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);
}
}