Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secret type to represent secrets and integrate with the secrets handling #1232

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ static byte[] generate(final ConfigMappingInterface mapping) {
generateToString(visitor, mapping);
generateNames(visitor, mapping);
generateDefaults(visitor, mapping);
generateSecrets(visitor, mapping);

return writer.toByteArray();
}
Expand Down Expand Up @@ -1021,6 +1022,48 @@ private static void generateDefaults(final ClassVisitor classVisitor, final Conf
mv.visitEnd();
}

private static void generateSecrets(final ClassVisitor classVisitor, final ConfigMappingInterface mapping) {
MethodVisitor mv = classVisitor.visitMethod(ACC_PUBLIC | ACC_STATIC, "getSecrets", "()Ljava/util/Map;",
"()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",
null);

mv.visitTypeInsn(NEW, "java/util/HashSet");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashSet", "<init>", "()V", false);
mv.visitVarInsn(ASTORE, 0);

for (Map.Entry<String, Property> entry : ConfigMappingInterface.getProperties(mapping)
.get(mapping.getInterfaceType())
.get("").entrySet()) {
if (entry.getValue().isLeaf()) {
LeafProperty property = entry.getValue().asLeaf();
if (property.isSecret()) {
mv.visitVarInsn(ALOAD, 0);
mv.visitLdcInsn(entry.getKey());
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true);
mv.visitInsn(POP);
}
}

if (entry.getValue().isMap()) {
if (entry.getValue().asMap().getValueProperty().isLeaf()) {
LeafProperty property = entry.getValue().asMap().getValueProperty().asLeaf();
if (property.isSecret()) {
mv.visitVarInsn(ALOAD, 0);
mv.visitLdcInsn(entry.getKey());
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Set", "add", "(Ljava/lang/Object;)Z", true);
mv.visitInsn(POP);
}
}
}
}

mv.visitVarInsn(ALOAD, 0);
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}

private static boolean hasDefaultValue(final Class<?> klass, final Object value) {
if (value == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ public Class<?> getValueRawType() {
return rawType;
}

public boolean isSecret() {
return Secret.class.isAssignableFrom(rawType);
}

@Override
public boolean isLeaf() {
return true;
Expand Down Expand Up @@ -772,6 +776,11 @@ private static ConfigMappingInterface createConfigurationInterface(Class<?> inte
return null;
}

// TODO - What should we do with this? We want to avoid having to use @WithConverter all the time, but the mapping metadata does not know which converters are available.
if (Secret.class.isAssignableFrom(interfaceType)) {
return null;
}

// first, find any supertypes
ConfigMappingInterface[] superTypes = getSuperTypes(interfaceType.getInterfaces(), 0, 0);
// now find any properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ static <T> Map<String, String> configMappingDefaults(final Class<T> interfaceTyp
}
}

static <T> Set<String> configMappingSecrets(final Class<T> interfaceType) {
try {
Method getSecrets = CACHE.get(interfaceType).getImplementationClass().getDeclaredMethod("getSecrets");
return (Set<String>) getSecrets.invoke(null);
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
} catch (IllegalAccessException e) {
throw new IllegalAccessError(e.getMessage());
} catch (InvocationTargetException e) {
try {
throw e.getCause();
} catch (RuntimeException | Error e2) {
throw e2;
} catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
}

static <T> T configMappingObject(final Class<T> interfaceType, final ConfigMappingContext configMappingContext) {
ConfigMappingObject instance;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static boolean equals(final String name, final String other) {
return true;
}

if (name.equals("*") && (other.equals("") || other.equals("\"\""))) {
if (name.equals("*") && (other.isEmpty() || other.equals("\"\""))) {
return false;
}

Expand Down
26 changes: 26 additions & 0 deletions implementation/src/main/java/io/smallrye/config/Secret.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.smallrye.config;

import org.eclipse.microprofile.config.spi.Converter;

public interface Secret<T> {
T get();

class SecretConverter<T> implements Converter<Secret<T>> {
private static final long serialVersionUID = -4624156385855243648L;
private final Converter<T> delegate;

public SecretConverter(final Converter<T> delegate) {
this.delegate = delegate;
}

@Override
public Secret<T> convert(final String value) throws IllegalArgumentException, NullPointerException {
return new Secret<T>() {
@Override
public T get() {
return delegate.convert(value);
}
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.smallrye.config;

import static java.util.stream.Collectors.toSet;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
Expand All @@ -12,15 +14,15 @@
public class SecretKeysConfigSourceInterceptor implements ConfigSourceInterceptor {
private static final long serialVersionUID = 7291982039729980590L;

private final Set<String> secrets;
private final Set<PropertyName> secrets;

public SecretKeysConfigSourceInterceptor(final Set<String> secrets) {
this.secrets = secrets;
this.secrets = secrets.stream().map(PropertyName::new).collect(toSet());
}

@Override
public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) {
if (SecretKeys.isLocked() && secrets.contains(name)) {
if (SecretKeys.isLocked() && secrets.contains(new PropertyName(name))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new PropertyTime every time getValue is called may impact performance, perhaps consider iterating the Set and comparing the name instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to move forward with the implementation. I haven't profiled the code yet, but I am not too worried about it because when we are populating the config, the secrets are unlocked, but I'll have a look. Thanks!

throw ConfigMessages.msg.notAllowed(name);
}
return context.proceed(name);
Expand All @@ -33,7 +35,7 @@ public Iterator<String> iterateNames(final ConfigSourceInterceptorContext contex
Iterator<String> namesIterator = context.iterateNames();
while (namesIterator.hasNext()) {
String name = namesIterator.next();
if (!secrets.contains(name)) {
if (!secrets.contains(new PropertyName(name))) {
names.add(name);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public ConfigMappingContext get() {
return new ConfigMappingContext(SmallRyeConfig.this, mappingsBuilder.getMappings());
}
});
// We clear the properties because they may contain secrets
this.configSources.getPropertyNames().clear();

if (getOptionalValue(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, boolean.class).orElse(true)) {
context.reportUnknown(mappingsBuilder.getIgnoredPaths());
Expand Down Expand Up @@ -1059,6 +1061,10 @@ Iterable<String> latest() {
names.remove(ConfigSource.CONFIG_ORDINAL);
return Collections.unmodifiableSet(names);
}

void clear() {
names.clear();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ public SmallRyeConfigBuilder withMapping(Class<?> klass) {
}

public SmallRyeConfigBuilder withMapping(Class<?> klass, String prefix) {
mappingsBuilder.mapping(klass, prefix);
mappingsBuilder.mapping(klass, prefix, this);
return this;
}

Expand Down Expand Up @@ -765,7 +765,7 @@ final class MappingBuilder {
private final Map<Class<?>, Set<String>> mappings = new HashMap<>();
private final List<String> ignoredPaths = new ArrayList<>();

MappingBuilder mapping(Class<?> type, String prefix) {
MappingBuilder mapping(Class<?> type, String prefix, final SmallRyeConfigBuilder configBuilder) {
Assert.checkNotNullParam("type", type);
Assert.checkNotNullParam("path", prefix);
Class<?> mappingClass = getConfigMappingClass(type);
Expand All @@ -774,12 +774,16 @@ MappingBuilder mapping(Class<?> type, String prefix) {
ignoredPaths.add(prefix.isEmpty() ? "*" : prefix + ".**");
}
mappings.computeIfAbsent(mappingClass, k -> new HashSet<>(4)).add(prefix);
// Load the mapping defaults, to make the defaults available to all config sources

// Load the mapping defaults, to make the defaults available to all config sources
for (Map.Entry<String, String> defaultEntry : ConfigMappingLoader.configMappingDefaults(mappingClass).entrySet()) {
// Do not override builder defaults with mapping defaults
defaultValues.putIfAbsent(prefix(prefix, defaultEntry.getKey()), defaultEntry.getValue());
}
// Load secret names
for (String secret : ConfigMappingLoader.configMappingSecrets(mappingClass)) {
configBuilder.withSecretKeys(prefix(prefix, secret));
}
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.smallrye.config;

import static io.smallrye.config.Converters.STRING_CONVERTER;
import static io.smallrye.config.KeyValuesConfigSource.config;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.StreamSupport.stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.junit.jupiter.api.Test;

class ConfigMappingSecretsTest {
@Test
void secrets() throws Exception {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.withConverter(Secret.class, 100, new Secret.SecretConverter(STRING_CONVERTER))
.withMapping(MappingSecrets.class)
.withSources(config(
"secrets.secret", "secret",
"secrets.list[0]", "secret",
"secrets.map.key", "secret",
"secrets.optional", "secret"))
.withSecretKeys()
.build();

MappingSecrets mapping = config.getConfigMapping(MappingSecrets.class);
assertEquals("secret", mapping.secret().get());
assertEquals("secret", mapping.list().get(0).get());
assertEquals("secret", mapping.map().get("key").get());
assertTrue(mapping.optional().isPresent());
assertEquals("secret", mapping.optional().get().get());

assertThrows(SecurityException.class, () -> config.getRawValue("secrets.secret"));
assertThrows(SecurityException.class, () -> config.getRawValue("secrets.list[0]"));
assertThrows(SecurityException.class, () -> config.getRawValue("secrets.map.key"));
assertThrows(SecurityException.class, () -> config.getRawValue("secrets.optional"));

Set<String> properties = stream(config.getPropertyNames().spliterator(), false).collect(toSet());
assertFalse(properties.contains("secrets.secret"));
assertFalse(properties.contains("secrets.secrets[0]"));
assertFalse(properties.contains("secrets.secret-map.key"));
assertFalse(properties.contains("secrets.optional"));
}

@ConfigMapping(prefix = "secrets")
interface MappingSecrets {
Secret<String> secret();

List<Secret<String>> list();

Map<String, Secret<String>> map();

Optional<Secret<String>> optional();
}
}