From e3655bf4313645808e9f2321f44441dca13b6296 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Mon, 21 Aug 2023 13:40:09 -0700 Subject: [PATCH] Addresses issue #4238. Backport of #7391. (#7414) Signed-off-by: Laird Nelson --- bom/pom.xml | 5 + .../etc/spotbugs/exclude.xml | 32 + .../oci/oci-secrets-config-source/pom.xml | 146 +++++ .../OciSecretsConfigSourceProvider.java | 584 ++++++++++++++++++ .../secrets/configsource/package-info.java | 26 + .../src/main/java/module-info.java | 40 ++ .../secrets/configsource/IsModifiedTest.java | 46 ++ .../oci/secrets/configsource/UsageTest.java | 79 +++ .../secrets/configsource/ValueNodeTest.java | 45 ++ .../src/test/java/logging.properties | 19 + .../src/test/resources/meta-config.yaml | 21 + integrations/oci/pom.xml | 1 + 12 files changed, 1044 insertions(+) create mode 100644 integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml create mode 100644 integrations/oci/oci-secrets-config-source/pom.xml create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java create mode 100644 integrations/oci/oci-secrets-config-source/src/main/java/module-info.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java create mode 100644 integrations/oci/oci-secrets-config-source/src/test/java/logging.properties create mode 100644 integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml diff --git a/bom/pom.xml b/bom/pom.xml index cac54b2a959..afa910711cb 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -912,6 +912,11 @@ helidon-integrations-oci-sdk-runtime ${helidon.version} + + io.helidon.integrations.oci + helidon-integrations-oci-secrets-configsource + ${helidon.version} + io.helidon.integrations.oci.metrics helidon-integrations-oci-metrics diff --git a/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml b/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..b967a4ee6bc --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/etc/spotbugs/exclude.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/integrations/oci/oci-secrets-config-source/pom.xml b/integrations/oci/oci-secrets-config-source/pom.xml new file mode 100644 index 00000000000..b77ac01e3e8 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/pom.xml @@ -0,0 +1,146 @@ + + + + 4.0.0 + + io.helidon.integrations.oci + helidon-integrations-oci-project + 3.2.3-SNAPSHOT + + helidon-integrations-oci-secrets-config-source + Helidon Integrations OCI Secrets Config Source + + + OCI Secrets Retrieval API ConfigSourceProvider Implementation + + + + + src/test/java/logging.properties + etc/spotbugs/exclude.xml + + + + + + + + + io.helidon.common + helidon-common + + + io.helidon.config + helidon-config + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + com.oracle.oci.sdk + oci-java-sdk-common + + + com.oracle.oci.sdk + oci-java-sdk-secrets + + + com.oracle.oci.sdk + oci-java-sdk-vault + + + jakarta.annotation + jakarta.annotation-api + + + + + + com.oracle.oci.sdk + oci-java-sdk-common-httpclient-jersey3 + runtime + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + + + + org.glassfish.jersey.connectors + jersey-apache-connector + runtime + + + + + + io.helidon.config + helidon-config-yaml-mp + test + + + org.hamcrest + hamcrest-core + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-jdk14 + test + + + + + + + + src/test/resources + true + + meta-config.yaml + + + + + + maven-surefire-plugin + + + ${compartment-ocid} + ${java.util.logging.config.file} + ${vault-ocid} + + + + + + diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java new file mode 100644 index 00000000000..e5e16f26b7c --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/OciSecretsConfigSourceProvider.java @@ -0,0 +1,584 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.lang.System.Logger; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; +import io.helidon.config.AbstractConfigSource; +import io.helidon.config.AbstractConfigSourceBuilder; +import io.helidon.config.Config; +import io.helidon.config.spi.ConfigContent.NodeContent; +import io.helidon.config.spi.ConfigNode.ObjectNode; +import io.helidon.config.spi.ConfigNode.ValueNode; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.ConfigSourceProvider; +import io.helidon.config.spi.NodeConfigSource; +import io.helidon.config.spi.PollableSource; +import io.helidon.config.spi.PollingStrategy; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.SecretsClient; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundleContentDetails; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.VaultsClient; +import com.oracle.bmc.vault.model.SecretSummary; +import com.oracle.bmc.vault.requests.ListSecretsRequest; +import jakarta.annotation.Priority; + +import static io.helidon.integrations.oci.sdk.runtime.OciExtension.ociAuthenticationProvider; +import static java.lang.System.Logger.Level.WARNING; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.now; +import static java.util.concurrent.Executors.newCachedThreadPool; + +/** + * A {@link ConfigSourceProvider} that {@linkplain #create(String, Config) creates} {@link ConfigSource} implementations + * that interact with the Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault APIs. + * + *

To use, ensure the packaging artifact (e.g. {@code .jar} file or similar) containing this class is present on your + * class or module path as appropriate, and configure a meta-configuration source with a {@code type} of {@code + * oci-secrets}, following the usual Helidon meta-configuration rules.

+ * + *

More specifically:

+ * + *
    + * + *
  1. Ensure you have an authentication mechanism set up to connect to OCI (e.g. a valid OCI configuration + * file). Authentication with OCI is accomplished via the {@link + * io.helidon.integrations.oci.sdk.runtime.OciExtension} class; please see its documentation for how and when to set up + * an {@code oci.yaml} classpath resource to further refine the mechanism of authentication.
  2. + * + *
  3. Ensure there is a classpath resource present named {@code meta-config.yaml}.
  4. + * + *
  5. Ensure the {@code meta-config.yaml} classpath resource contains a {@code sources} element with a {@code type} of + * {@code oci-secrets} that looks similar to the following, substituting values as appropriate:
    sources:
    + *  - type: 'oci-secrets'
    + *    properties:
    + *      compartment-ocid: 'your vault compartment OCID goes here'
    + *      vault-ocid: 'your vault OCID goes here'
  6. + * + *
+ * + *

Refer to Helidon's documentation concerning meta-configuration for more details.

+ * + * @see ConfigSourceProvider + */ +@Priority(300) +public final class OciSecretsConfigSourceProvider implements ConfigSourceProvider { + + + /* + * Static fields. + */ + + + private static final Set SUPPORTED_TYPES = Set.of("oci-secrets"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link OciSecretsConfigSourceProvider}. + * + * @deprecated For use by {@link java.util.ServiceLoader} only. + */ + @Deprecated // For use by java.util.ServiceLoader only. + public OciSecretsConfigSourceProvider() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Creates and returns a non-{@code null} {@link ConfigSource} that sources its values from an Oracle Cloud + * Infrastructure (OCI) Vault. + * + * @param type one of the {@linkplain #supported() supported types}; not actually used + * + * @param metaConfig a {@link Config} serving as meta-configuration for this provider; must not be {@code null} when + * {@code type} is {@linkplain #supports(String) supported} + * + * @return a non-{@code null} {@link ConfigSource} + * + * @exception NullPointerException if {@code type} is {@linkplain #supports(String) supported} and {@code + * metaConfig} is {@code null} + * + * @see #supported() + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public ConfigSource create(String type, Config metaConfig) { + return SecretBundleConfigSource.builder().config(metaConfig).build(); + } + + /** + * Returns a non-{@code null}, immutable {@link Set} of supported types suitable for the Helidon Config subsystem to + * pass to the {@link #create(String, Config)} method. + * + *

This method returns a {@link Set} whose sole element is the string "{@code oci-secrets}".

+ * + * @return a non-{@code null}, immutable {@link Set} + * + * @see #create(String, Config) + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public Set supported() { + return SUPPORTED_TYPES; + } + + /** + * Returns {@code true} if and only if the {@link Set} returned by an invocation of the {@link #supported()} method + * {@linkplain Set#contains(Object) contains} it. + * + * @param type the type to test + * + * @return {@code true} if and only if the {@link Set} returned by an invocation of the {@link #supported()} method + * {@linkplain Set#contains(Object) contains} it + * + * @see #supported() + * + * @see #create(String, Config) + * + * @deprecated For use by the Helidon Config subsystem only. + */ + @Deprecated // For use by the Helidon Config subsystem only. + @Override // ConfigSourceProvider + public boolean supports(String type) { + return this.supported().contains(type); + } + + + /* + * Inner and nested classes. + */ + + + static final class SecretBundleConfigSource + extends AbstractConfigSource implements NodeConfigSource, PollableSource { + + + /* + * Static fields. + */ + + + private static final Optional ABSENT_NODE_CONTENT = + Optional.of(NodeContent.builder().node(ObjectNode.empty()).build()); + + private static final String COMPARTMENT_OCID_PROPERTY_NAME = "compartment-ocid"; + + private static final Logger LOGGER = System.getLogger(SecretBundleConfigSource.class.getName()); + + private static final String VAULT_OCID_PROPERTY_NAME = "vault-ocid"; + + + /* + * Instance fields. + */ + + + private final ExecutorService es; + + private final Supplier> loader; + + private volatile Instant closestSecretExpirationInstant; + + + /* + * Constructors. + */ + + + private SecretBundleConfigSource(Builder b) { + super(b); + // From Executors#newCachedThreadPool() javadoc: "Creates a thread pool that creates new threads as needed, + // but will reuse previously constructed threads when they are available. These pools will typically improve + // the performance of programs that execute many short-lived asynchronous tasks." That describes our use + // case exactly. + this.es = newCachedThreadPool(); + // Helidon Config has no defined lifecycle so the best we can do is forcibly close the ExecutorService on VM + // exit. + Runtime.getRuntime().addShutdownHook(new Thread(() -> this.es.shutdownNow())); + Supplier secretsSupplier = Objects.requireNonNull(b.secretsSupplier, "b.secretsSupplier"); + Supplier vaultsSupplier = Objects.requireNonNull(b.vaultsSupplier, "b.vaultsSupplier"); + this.closestSecretExpirationInstant = now(); + String compartmentOcid = b.compartmentOcid; + String vaultOcid = b.vaultOcid; + if (compartmentOcid == null || vaultOcid == null) { + // (It is not immediately clear why the OCI Java SDK requires a Compartment OCID, since a Vault OCID is + // sufficient to uniquely identify any Vault.) + this.loader = this::absentNodeContent; + } else { + ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder() + .compartmentId(compartmentOcid) + .vaultId(vaultOcid) + .build(); + this.loader = () -> this.load(vaultsSupplier, secretsSupplier, listSecretsRequest); + } + } + + + /* + * Instance methods. + */ + + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // PollableSource + public boolean isModified(Instant pollInstant) { + return isModified(pollInstant, this.closestSecretExpirationInstant); // volatile read + } + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // NodeConfigSource + public Optional load() { + return this.loader.get(); + } + + @Deprecated // For use by the Helidon Config subsystem only. + @Override // PollableSource + public Optional pollingStrategy() { + return super.pollingStrategy(); + } + + private Optional absentNodeContent() { + return ABSENT_NODE_CONTENT; + } + + private void completeTasks(Collection> tasks, AutoCloseable autoCloseable) { + try (autoCloseable) { + completeTasks(this.es, tasks); + } catch (RuntimeException e) { + throw e; + } catch (InterruptedException e) { + // (Can legally be thrown by any AutoCloseable. Must preserve interrupt status.) + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (Exception e) { + // (Can legally be thrown by any AutoCloseable.) + throw new IllegalStateException(e.getMessage(), e); + } + } + + private Optional load(Supplier vaultsSupplier, + Supplier secretsSupplier, + ListSecretsRequest listSecretsRequest) { + Collection secretSummaries = secretSummaries(vaultsSupplier, listSecretsRequest); + return this.load(secretSummaries, secretsSupplier); + } + + private Optional load(Collection secretSummaries, + Supplier secretsSupplier) { + if (secretSummaries.isEmpty()) { + return this.absentNodeContent(); + } + ConcurrentMap valueNodes = new ConcurrentHashMap<>(); + Collection> tasks = new ArrayList<>(secretSummaries.size()); + Base64.Decoder decoder = Base64.getDecoder(); + Secrets secrets = secretsSupplier.get(); + Instant closestSecretExpirationInstant = this.closestSecretExpirationInstant; // volatile read + for (SecretSummary ss : secretSummaries) { + tasks.add(task(valueNodes::put, + ss.getSecretName(), + r -> secrets.getSecretBundle(r).getSecretBundle().getSecretBundleContent(), + ss.getId(), + decoder)); + java.util.Date d = ss.getTimeOfCurrentVersionExpiry(); + // If d is null, which is permitted by the OCI Vaults API, you could interpret it as meaning "this + // secret never ever expires, so never poll it for changes ever again". (This is sort of like if its + // expiration time were set to the end of time.) + // + // Or you could interpret it as the much more common "this secret never had its expiration time set, + // probably by mistake, or because it's a temporary scratch secret, or any of a zillion other possible + // common human explanations, so we'd better check each time we poll to see if the secret is still + // there; i.e. we should pretend it is continually expiring". (This is sort of like if its expiration + // time were set to the beginning of time.) + // + // We opt for the latter interpretation. + Instant secretExpirationInstant = d == null ? null : d.toInstant(); + if (secretExpirationInstant != null && secretExpirationInstant.isBefore(closestSecretExpirationInstant)) { + closestSecretExpirationInstant = secretExpirationInstant; + } + } + this.closestSecretExpirationInstant = closestSecretExpirationInstant; // volatile write + this.completeTasks(tasks, secrets); + ObjectNode.Builder onb = ObjectNode.builder(); + for (Entry e : valueNodes.entrySet()) { + onb.addValue(e.getKey(), e.getValue()); + } + return Optional.of(NodeContent.builder() + .node(onb.build()) + .build()); + } + + + /* + * Static methods. + */ + + + private static Builder builder() { + return new Builder(); + } + + private static void closeUnchecked(AutoCloseable autoCloseable) { + try { + autoCloseable.close(); + } catch (RuntimeException e) { + throw e; + } catch (InterruptedException e) { + // (Can legally be thrown by any AutoCloseable. Must preserve interrupt status.) + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (Exception e) { + // (Can legally be thrown by any AutoCloseable.) + throw new IllegalStateException(e.getMessage(), e); + } + } + + private static void completeTasks(ExecutorService es, Collection> tasks) { + RuntimeException re = null; + for (Future future : invokeAllUnchecked(es, tasks)) { + try { + futureGetUnchecked(future); + } catch (RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } + } + if (re != null) { + throw re; + } + } + + private static T futureGetUnchecked(Future future) { + try { + return future.get(); + } catch (ExecutionException e) { + throw new IllegalStateException(e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } + } + + private static List> invokeAllUnchecked(ExecutorService es, Collection> tasks) { + try { + return es.invokeAll(tasks); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } + } + + static boolean isModified(Instant pollInstant, Instant closestSecretExpirationInstant) { + return closestSecretExpirationInstant.isBefore(pollInstant); + } + + private static GetSecretBundleRequest request(String secretId) { + return GetSecretBundleRequest.builder().secretId(secretId).build(); + } + + // Suppress "[try] auto-closeable resource Vaults has a member method close() that could throw + // InterruptedException" since we handle it. + @SuppressWarnings("try") + private static Collection secretSummaries(Supplier vaultsSupplier, + ListSecretsRequest listSecretsRequest) { + try (Vaults v = vaultsSupplier.get()) { + return v.listSecrets(listSecretsRequest).getItems(); + } catch (RuntimeException e) { + throw e; + } catch (InterruptedException e) { + // (Can legally be thrown by any AutoCloseable (such as Vaults). Must preserve interrupt status.) + Thread.currentThread().interrupt(); + throw new IllegalStateException(e.getMessage(), e); + } catch (Exception e) { + // (Can legally be thrown by any AutoCloseable (such as Vaults).) + throw new IllegalStateException(e.getMessage(), e); + } + } + + static Callable task(BiConsumer valueNodes, + String secretName, + Function f, + String secretId, + Base64.Decoder base64Decoder) { + return () -> { + valueNodes.accept(secretName, valueNode(f, secretId, base64Decoder)); + return null; + }; + } + + private static ValueNode valueNode(Function f, + String secretId, + Base64.Decoder base64Decoder) { + return valueNode((Base64SecretBundleContentDetails) f.apply(request(secretId)), base64Decoder); + } + + private static ValueNode valueNode(Base64SecretBundleContentDetails details, Base64.Decoder base64Decoder) { + return valueNode(details.getContent(), base64Decoder); + } + + static ValueNode valueNode(String base64EncodedContent, Base64.Decoder base64Decoder) { + String decodedContent = new String(base64Decoder.decode(base64EncodedContent), UTF_8); + return ValueNode.create(decodedContent.intern()); + } + + + /* + * Inner and nested classes. + */ + + + private static final class Builder extends AbstractConfigSourceBuilder { + + + /* + * Instance fields. + */ + + + private String compartmentOcid; + + private Supplier secretsSupplier; + + private String vaultOcid; + + private Supplier vaultsSupplier; + + + /* + * Constructors. + */ + + + private Builder() { + super(); + Supplier adpSupplier = + LazyValue.create(() -> (BasicAuthenticationDetailsProvider) ociAuthenticationProvider().get()); + SecretsClient.Builder scb = SecretsClient.builder(); + this.secretsSupplier = () -> scb.build(adpSupplier.get()); + VaultsClient.Builder vcb = VaultsClient.builder(); + this.vaultsSupplier = () -> vcb.build(adpSupplier.get()); + } + + + /* + * Instance methods. + */ + + + @Override // AbstractConfigSourceBuilder + protected Builder config(Config metaConfig) { + metaConfig.get("compartment-ocid") + .asString() + .filter(Predicate.not(String::isBlank)) + .ifPresentOrElse(this::compartmentOcid, + () -> { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No meta-configuration value supplied for " + + metaConfig.key().toString() + "." + COMPARTMENT_OCID_PROPERTY_NAME + + "); resulting ConfigSource will be empty"); + } + }); + metaConfig.get("vault-ocid") + .asString() + .filter(Predicate.not(String::isBlank)) + .ifPresentOrElse(this::vaultOcid, + () -> { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No meta-configuration value supplied for " + + metaConfig.key().toString() + "." + VAULT_OCID_PROPERTY_NAME + + "); resulting ConfigSource will be empty"); + } + }); + return super.config(metaConfig); + } + + private SecretBundleConfigSource build() { + return new SecretBundleConfigSource(this); + } + + private Builder compartmentOcid(String compartmentOcid) { + this.compartmentOcid = Objects.requireNonNull(compartmentOcid, "compartmentOcid"); + return this; + } + + private Builder secretsSupplier(Supplier secretsSupplier) { + this.secretsSupplier = Objects.requireNonNull(secretsSupplier, "secretsSupplier"); + return this; + } + + private Builder vaultOcid(String vaultOcid) { + this.vaultOcid = Objects.requireNonNull(vaultOcid, "vaultOcid"); + return this; + } + + private Builder vaultsSupplier(Supplier vaultsSupplier) { + this.vaultsSupplier = Objects.requireNonNull(vaultsSupplier, "vaultsSupplier"); + return this; + } + + } + + } + +} diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java new file mode 100644 index 00000000000..a6da275765c --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/io/helidon/integrations/oci/secrets/configsource/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Provides classes and interfaces for using the Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault APIs + * as part of a {@linkplain io.helidon.config.spi.ConfigSourceProvider} implementation. + * + * @see io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider + */ +package io.helidon.integrations.oci.secrets.configsource; diff --git a/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java new file mode 100644 index 00000000000..cbacd5343b3 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/main/java/module-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Provides packages containing classes and interfaces for Oracle Cloud Infrastructure (OCI) Secrets + * Retrieval and Vault + * API-using {@linkplain io.helidon.config.spi.ConfigSource configuration sources}. + * + * @see io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider + */ +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) +module io.helidon.integrations.oci.secrets.configsource { + + exports io.helidon.integrations.oci.secrets.configsource; + + requires io.helidon.common; + requires transitive io.helidon.config; + requires io.helidon.integrations.oci.sdk.runtime; + requires oci.java.sdk.common; + requires oci.java.sdk.secrets; + requires oci.java.sdk.vault; + + provides io.helidon.config.spi.ConfigSourceProvider with io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider; + +} diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java new file mode 100644 index 00000000000..00379b53a6d --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/IsModifiedTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import static io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider.SecretBundleConfigSource.isModified; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class IsModifiedTest { + + @Test + void testIsModified() { + // Test java.time behavior. + Instant now0 = Instant.now(); + Instant now1 = Instant.from(now0); + assertThat(now1, is(now0)); + Instant later = now0.plusSeconds(500); // arbitrary amount + assertThat(later.isAfter(now0), is(true)); + Instant earlier = now0.minusSeconds(500); // arbitrary amount + assertThat(earlier.isBefore(now0), is(true)); + + // Test that isModified properly encapsulates java.time behavior. + assertThat(isModified(now0, later), is(false)); + assertThat(isModified(now0, now1), is(false)); + assertThat(isModified(now0, earlier), is(true)); + } + +} + diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java new file mode 100644 index 00000000000..58b6ca856db --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/UsageTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class UsageTest { + + @Test + void testUsage() { + // Get a Config object. Because src/test/resources/meta-config.yaml exists, and because it will be processed + // according to the Helidon rules, an + // io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider will be created and any + // ConfigSources it creates will become part of the assembled Config object. + Config c = Config.create(); + + // Make sure non-existent properties don't cause the Vault to get involved. + assertThat(c.get("bogus").asNode().orElse(null), nullValue()); + + // Make sure properties that have nothing to do with the OCI Secrets Retrieval or Vault APIs are handled by some + // other (default) ConfigSource, e.g., System properties, etc. (The OCI Secrets Retrieval API should never be + // consulted for java.home, in other words.) + assertThat(c.get("java.home").asString().orElse(null), is(System.getProperty("java.home"))); + + // Do the rest of this test only if the following assumptions hold. To avoid skipping the rest of this test: + // + // 1. Set up a ${HOME}/.oci/config file following + // https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm or similar + // + // 2. Run Maven with all of the following properties: + // + // -Dcompartment-ocid=ocid1.compartment.oci1.iad.123xyz (a valid OCI Compartment OCID) + // -Dvault-ocid=ocid1.vault.oci1.iad.123xyz (a valid OCI Vault OCID) + // -DFrancqueSecret.expectedValue='Some Value' (some value for a secret named FrancqueSecret in that vault) + // + assumeTrue(Files.exists(Paths.get(System.getProperty("user.home"), ".oci", "config"))); // condition 1 + assumeFalse(System.getProperty("compartment-ocid", "").isBlank()); // condition 2 + assumeFalse(System.getProperty("vault-ocid", "").isBlank()); // condition 2 + String expectedValue = System.getProperty("FrancqueSecret.expectedValue", ""); + assumeFalse(expectedValue.isBlank()); // condition 2 + + // + // (Code below this line executes only if the above JUnit assumptions passed. Otherwise control flow stops above.) + // + + // For this test to pass, all of the following must hold: + // + // 1. The vault designated by the vault OCID must hold a secret named FrancqueSecret + // + // 2. The secret named FrancqueSecret must have a value equal to the expected value + assertThat(c.get("FrancqueSecret").asString().orElse(null), is(expectedValue)); + } + +} diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java new file mode 100644 index 00000000000..67d147bf678 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/io/helidon/integrations/oci/secrets/configsource/ValueNodeTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.integrations.oci.secrets.configsource; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +import static io.helidon.integrations.oci.secrets.configsource.OciSecretsConfigSourceProvider.SecretBundleConfigSource.valueNode; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class ValueNodeTest { + + @Test + void testValueNode() { + // Test the JDK's base64 decoding behavior. + String raw = new String("abc".getBytes(), UTF_8); + byte[] bytes = Base64.getEncoder().encode(raw.getBytes()); + String encoded = new String(bytes, UTF_8); + Base64.Decoder decoder = Base64.getDecoder(); + bytes = decoder.decode(encoded); + String decoded = new String(bytes, UTF_8); + assertThat(decoded, is(raw)); + + // Test that valueNode properly encapsulates this platform behavior. + assertThat(valueNode(encoded, decoder).get(), is(decoded)); + } + +} + diff --git a/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties b/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties new file mode 100644 index 00000000000..686736250f3 --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/java/logging.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +com.oracle.bmc.level = SEVERE +handlers = java.util.logging.ConsoleHandler +io.helidon.integrations.oci.secrets.configsource.level = FINER +java.util.logging.ConsoleHandler.level = FINER diff --git a/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml new file mode 100644 index 00000000000..748334ff93d --- /dev/null +++ b/integrations/oci/oci-secrets-config-source/src/test/resources/meta-config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +sources: + - type: 'system-properties' # for testing + - type: 'oci-secrets' + properties: # required + compartment-ocid: ${compartment-ocid} + vault-ocid: ${vault-ocid} diff --git a/integrations/oci/pom.xml b/integrations/oci/pom.xml index 783f4cc7074..d861f2864b3 100644 --- a/integrations/oci/pom.xml +++ b/integrations/oci/pom.xml @@ -34,6 +34,7 @@ metrics + oci-secrets-config-source sdk