Skip to content

Commit

Permalink
Fixes #269. Initial work to support secrets.
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez committed Apr 16, 2020
1 parent b21b84e commit cd22f7d
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 4 deletions.
12 changes: 12 additions & 0 deletions implementation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-junit4</artifactId>
<version>2.0.1.Final</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

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

import java.io.Serializable;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

public class SecretKeys implements Serializable {
private static final ThreadLocal<Boolean> LOCKED = ThreadLocal.withInitial(() -> Boolean.TRUE);

private final Function<String, Boolean> secrets;

public SecretKeys() {
this.secrets = (Function<String, Boolean> & Serializable) s -> false;
}

public SecretKeys(final Function<String, Boolean> secrets) {
this.secrets = secrets != null ? (Function<String, Boolean> & Serializable) secrets
: (Function<String, Boolean> & Serializable) s -> false;
}

public SecretKeys(final Set<String> secrets) {
this.secrets = (Function<String, Boolean> & Serializable) secrets::contains;
}

public boolean isSecret(String propertyName) {
return secrets.apply(propertyName);
}

public boolean isSecretAccessible(String propertyName) {
return !isSecret(propertyName) || !LOCKED.get();
}

public void accessSecrets(Runnable runnable) {
LOCKED.set(false);
try {
runnable.run();
} finally {
LOCKED.set(true);
}
}

public String accessSecret(Supplier<String> supplier) {
LOCKED.set(false);
try {
return supplier.get();
} finally {
LOCKED.set(true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigSource;
Expand Down Expand Up @@ -68,18 +69,21 @@ public int compare(ConfigSource o1, ConfigSource o2) {
private final Map<Type, Converter<?>> converters;
private final Map<Type, Converter<Optional<?>>> optionalConverters = new ConcurrentHashMap<>();
private final ConfigSourceInterceptorContext interceptorChain;
private final SecretKeys secretKeys;

SmallRyeConfig(SmallRyeConfigBuilder builder) {
this.configSourcesRef = buildConfigSources(builder);
this.interceptorChain = buildInterceptorChain(builder);
this.converters = buildConverters(builder);
this.secretKeys = buildSecretKeys(builder);
this.interceptorChain = buildInterceptorChain(builder);
}

@Deprecated
protected SmallRyeConfig(List<ConfigSource> configSources, Map<Type, Converter<?>> converters) {
this.configSourcesRef = new AtomicReference<>(Collections.unmodifiableList(configSources));
this.converters = new ConcurrentHashMap<>(Converters.ALL_CONVERTERS);
this.converters.putAll(converters);
this.secretKeys = new SecretKeys();
this.interceptorChain = buildInterceptorChain(new SmallRyeConfigBuilder());
}

Expand Down Expand Up @@ -124,6 +128,10 @@ private Map<Type, Converter<?>> buildConverters(final SmallRyeConfigBuilder buil
return converters;
}

private SecretKeys buildSecretKeys(final SmallRyeConfigBuilder builder) {
return new SecretKeys(builder.getSecretKeys());
}

private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfigBuilder builder) {
final List<InterceptorWithPriority> interceptors = new ArrayList<>(builder.getInterceptors());
if (builder.isAddDiscoveredInterceptors()) {
Expand All @@ -135,7 +143,10 @@ private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfi

interceptors.sort(Comparator.comparingInt(InterceptorWithPriority::getPriority).reversed());

SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(
SmallRyeConfigSourceInterceptorContext current;

// Last interceptor to lookup the key
current = new SmallRyeConfigSourceInterceptorContext(
(ConfigSourceInterceptor) (context, name) -> {
for (ConfigSource configSource : getConfigSources()) {
if (configSource instanceof ConfigValueConfigSource) {
Expand All @@ -159,6 +170,14 @@ private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfi
return null;
}, null);

// Security interceptor to prevent access to secret keys
current = new SmallRyeConfigSourceInterceptorContext((ConfigSourceInterceptor) (context, name) -> {
if (!secretKeys.isSecretAccessible(name)) {
throw new SecurityException("Not allowed to access secret key " + name);
}
return context.proceed(name);
}, current);

for (int i = interceptors.size() - 1; i >= 0; i--) {
current = new SmallRyeConfigSourceInterceptorContext(interceptors.get(i).getInterceptor(current), current);
}
Expand Down Expand Up @@ -244,7 +263,9 @@ public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, C
public Iterable<String> getPropertyNames() {
Set<String> names = new HashSet<>();
for (ConfigSource configSource : getConfigSources()) {
names.addAll(configSource.getPropertyNames());
names.addAll(
configSource.getPropertyNames().stream().filter(secretKeys::isSecretAccessible)
.collect(Collectors.toSet()));
}
return names;
}
Expand All @@ -254,6 +275,10 @@ public Iterable<ConfigSource> getConfigSources() {
return configSourcesRef.get();
}

public SecretKeys getSecretKeys() {
return secretKeys;
}

/**
* Add a configuration source to the configuration object. The list of configuration sources is re-sorted
* to insert the new source into the correct position. Configuration source wrappers configured with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
Expand All @@ -50,6 +52,7 @@ public class SmallRyeConfigBuilder implements ConfigBuilder {
private List<ConfigSource> sources = new ArrayList<>();
private Function<ConfigSource, ConfigSource> sourceWrappers = UnaryOperator.identity();
private Map<Type, ConverterWithPriority> converters = new HashMap<>();
private Set<String> secretKeys = new HashSet<>();
private List<InterceptorWithPriority> interceptors = new ArrayList<>();
private ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
private boolean addDefaultSources = false;
Expand Down Expand Up @@ -185,6 +188,11 @@ public SmallRyeConfigBuilder withInterceptorFactories(ConfigSourceInterceptorFac
return this;
}

public SmallRyeConfigBuilder withSecretKeys(String... keys) {
secretKeys.addAll(Stream.of(keys).collect(Collectors.toSet()));
return this;
}

@Override
public SmallRyeConfigBuilder withConverters(Converter<?>[] converters) {
for (Converter<?> converter : converters) {
Expand Down Expand Up @@ -246,6 +254,10 @@ Map<Type, ConverterWithPriority> getConverters() {
return converters;
}

Set<String> getSecretKeys() {
return secretKeys;
}

List<InterceptorWithPriority> getInterceptors() {
return interceptors;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static <T> T getValue(InjectionPoint injectionPoint, Config config) {
}
final SmallRyeConfig src = (SmallRyeConfig) config;
Converter<T> converter = resolveConverter(injectionPoint, src);
String rawValue = src.getRawValue(name);
String rawValue = getRawValue(src, name);
if (rawValue == null) {
rawValue = getDefaultValue(injectionPoint);
}
Expand All @@ -59,6 +59,10 @@ public static <T> T getValue(InjectionPoint injectionPoint, Config config) {
return converted;
}

private static String getRawValue(SmallRyeConfig config, String name) {
return config.getSecretKeys().accessSecret(() -> config.getRawValue(name));
}

private static NoSuchElementException propertyNotFound(final String name) {
return new NoSuchElementException("Required property " + name + " not found");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.smallrye.config;

import static java.util.stream.StreamSupport.stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import org.eclipse.microprofile.config.Config;
import org.junit.Test;

public class SecretKeysTest {
@Test
public void locked() {
final Config config = buildConfig("secret", "12345678", "not.secret", "value");

assertThrows("Not allowed to access secret key secret", SecurityException.class,
() -> config.getValue("secret", String.class));
assertEquals("value", config.getValue("not.secret", String.class));
}

@Test
public void unlock() {
final Config config = buildConfig("secret", "12345678", "not.secret", "value");
final SmallRyeConfig smallRyeConfig = (SmallRyeConfig) config;

smallRyeConfig.getSecretKeys().accessSecrets(
() -> assertEquals("12345678", config.getValue("secret", String.class)));

assertThrows("Not allowed to access secret key secret", SecurityException.class,
() -> config.getValue("secret", String.class));
}

@Test
public void filterSecretsPropertyNames() {
final Config config = buildConfig("secret", "12345678", "not.secret", "value");
config.getPropertyNames().forEach(s -> assertNotEquals("secret", s));
}

@Test
public void allPropertyNames() {
final Config config = buildConfig("secret", "12345678", "not.secret", "value");
final SmallRyeConfig smallRyeConfig = (SmallRyeConfig) config;

smallRyeConfig.getSecretKeys().accessSecrets(
() -> assertTrue(stream(smallRyeConfig.getPropertyNames().spliterator(), false)
.anyMatch(s -> s.equals("secret"))));
}

private static Config buildConfig(String... keyValues) {
return new SmallRyeConfigBuilder()
.addDefaultSources()
.addDefaultInterceptors()
.withSources(KeyValuesConfigSource.config(keyValues))
.withSecretKeys("secret")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.smallrye.config.inject;

import static org.junit.Assert.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.weld.junit4.WeldInitiator;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;

public class ConfigInjectionTest {
@BeforeClass
public static void beforeClass() throws Exception {
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
final URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {
new URL("memory", null, 0, "/",
new InMemoryStreamHandler("io.smallrye.config.inject.ConfigInjectionTestConfigFactory"))
}, contextClassLoader);
Thread.currentThread().setContextClassLoader(urlClassLoader);
}

@After
public void afterClass() {
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(contextClassLoader.getParent());
}

@Rule
public WeldInitiator weld = WeldInitiator.from(ConfigProducer.class, ConfigBean.class)
.addBeans()
.activate(ApplicationScoped.class)
.inject(this)
.build();

@Inject
private ConfigBean configBean;

@Test
public void inject() {
assertEquals("value", configBean.getConfig());
assertEquals("12345678", configBean.getSecret());
}

@ApplicationScoped
public static class ConfigBean {
@Inject
@ConfigProperty(name = "config")
private String config;
@Inject
@ConfigProperty(name = "secret")
private String secret;

public String getConfig() {
return config;
}

public String getSecret() {
return secret;
}
}

public static class InMemoryStreamHandler extends URLStreamHandler {
final byte[] contents;

public InMemoryStreamHandler(final String contents) {
this.contents = contents.getBytes();
}

@Override
protected URLConnection openConnection(final URL u) throws IOException {
if (!u.getFile().endsWith("SmallRyeConfigFactory")) {
return null;
}

return new URLConnection(u) {
@Override
public void connect() throws IOException {
}

@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(contents);
}
};
}
}
}
Loading

0 comments on commit cd22f7d

Please sign in to comment.