diff --git a/servers/quarkus-common/build.gradle.kts b/servers/quarkus-common/build.gradle.kts index 320a5e7d4be..016a7597e84 100644 --- a/servers/quarkus-common/build.gradle.kts +++ b/servers/quarkus-common/build.gradle.kts @@ -31,6 +31,10 @@ dependencies { implementation(project(":nessie-catalog-files-impl")) implementation(project(":nessie-catalog-service-common")) implementation(project(":nessie-catalog-secrets-api")) + implementation(project(":nessie-catalog-secrets-cache")) + implementation(project(":nessie-catalog-secrets-aws")) + implementation(project(":nessie-catalog-secrets-gcs")) + implementation(project(":nessie-catalog-secrets-vault")) compileOnly(project(":nessie-doc-generator-annotations")) @@ -55,14 +59,20 @@ dependencies { implementation("io.quarkus:quarkus-opentelemetry") implementation("io.quarkus:quarkus-micrometer") implementation("io.smallrye.config:smallrye-config-source-keystore") + implementation(enforcedPlatform(libs.quarkus.amazon.services.bom)) + implementation("io.quarkiverse.amazonservices:quarkus-amazon-secretsmanager") implementation("io.quarkiverse.amazonservices:quarkus-amazon-dynamodb") implementation("software.amazon.awssdk:sts") implementation("software.amazon.awssdk:apache-client") { exclude("commons-logging", "commons-logging") } implementation(enforcedPlatform(libs.quarkus.google.cloud.services.bom)) + implementation("io.quarkiverse.googlecloudservices:quarkus-google-cloud-secret-manager") implementation("io.quarkiverse.googlecloudservices:quarkus-google-cloud-bigtable") + + implementation("io.quarkiverse.vault:quarkus-vault") + implementation(enforcedPlatform(libs.quarkus.cassandra.bom)) implementation("com.datastax.oss.quarkus:cassandra-quarkus-client") { // spotbugs-annotations has only a GPL license! diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsCacheConfig.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsCacheConfig.java new file mode 100644 index 00000000000..59beb2eb3af --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsCacheConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.config; + +import io.smallrye.config.WithDefault; +import java.time.Duration; +import java.util.OptionalLong; + +/** Configurations for secrets caching. */ +public interface QuarkusSecretsCacheConfig { + /** Flag whether the secrets cache is enabled. */ + @WithDefault("true") + boolean enabled(); + + /** Optionally restrict the number of cached secrets. */ + OptionalLong maxElements(); + + /** Approximated maximum size on the Java heap for cached secrets. */ + @WithDefault("16") + long capacityMb(); + + /** Time until cached secrets expire. */ + @WithDefault("PT15M") + Duration ttl(); +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsConfig.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsConfig.java new file mode 100644 index 00000000000..dd4646e3bdd --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusSecretsConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.config; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "nessie.secrets") +public interface QuarkusSecretsConfig { + /** Choose the secrets manager to use. */ + @WithDefault("NONE") + SecretsSupplierType type(); + + @WithDefault("nessie/secrets") + String path(); + + QuarkusSecretsCacheConfig cache(); + + enum SecretsSupplierType { + NONE, + VAULT, + GOOGLE, + AMAZON + } +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/AmazonSecretsSupplierBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/AmazonSecretsSupplierBuilder.java new file mode 100644 index 00000000000..a516d5e4556 --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/AmazonSecretsSupplierBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import org.projectnessie.catalog.secrets.aws.AwsSecretsSupplier; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + +@Dependent +@SecretsType(SecretsSupplierType.AMAZON) +public class AmazonSecretsSupplierBuilder implements SecretsSupplierBuilder { + @Inject SecretsManagerClient client; + + @Override + public SecretsSupplier buildSupplier() { + return new AwsSecretsSupplier(client); + } +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/GoogleSecretsSupplierBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/GoogleSecretsSupplierBuilder.java new file mode 100644 index 00000000000..0d70977fff3 --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/GoogleSecretsSupplierBuilder.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import org.projectnessie.catalog.secrets.gcs.GcsSecretsSupplier; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; + +@Dependent +@SecretsType(SecretsSupplierType.GOOGLE) +public class GoogleSecretsSupplierBuilder implements SecretsSupplierBuilder { + + @Inject SecretManagerServiceClient client; + + @Override + public SecretsSupplier buildSupplier() { + return new GcsSecretsSupplier(client); + } +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/NoneSecretsSupplierBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/NoneSecretsSupplierBuilder.java new file mode 100644 index 00000000000..9458b79052a --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/NoneSecretsSupplierBuilder.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import jakarta.enterprise.context.Dependent; +import java.util.Map; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; + +@Dependent +@SecretsType(SecretsSupplierType.NONE) +public class NoneSecretsSupplierBuilder implements SecretsSupplierBuilder { + @Override + public SecretsSupplier buildSupplier() { + return names -> Map.of(); + } +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsSupplierBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsSupplierBuilder.java new file mode 100644 index 00000000000..79ecc646c10 --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsSupplierBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; + +public interface SecretsSupplierBuilder { + SecretsSupplier buildSupplier(); +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsType.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsType.java new file mode 100644 index 00000000000..4bb79f5817c --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/SecretsType.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; + +/** Store type qualifier for {@code VersionStoreFactory} classes. */ +@Target({TYPE, METHOD, PARAMETER, FIELD}) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface SecretsType { + /** Gets the store type. */ + SecretsSupplierType value(); + + /** Supports inline instantiation of the {@link SecretsType} qualifier. */ + final class Literal extends AnnotationLiteral implements SecretsType { + + private static final long serialVersionUID = 1L; + private final SecretsSupplierType value; + + public Literal(SecretsSupplierType value) { + this.value = value; + } + + @Override + public SecretsSupplierType value() { + return value; + } + } +} diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/VaultSecretsSupplierBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/VaultSecretsSupplierBuilder.java new file mode 100644 index 00000000000..3097f3ef283 --- /dev/null +++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/secrets/VaultSecretsSupplierBuilder.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.quarkus.providers.secrets; + +import io.quarkus.vault.VaultKVSecretReactiveEngine; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; +import org.projectnessie.catalog.secrets.vault.VaultSecretsSupplier; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; + +@Dependent +@SecretsType(SecretsSupplierType.VAULT) +public class VaultSecretsSupplierBuilder implements SecretsSupplierBuilder { + @Inject VaultKVSecretReactiveEngine engine; + + @Inject QuarkusSecretsConfig secretsConfig; + + @Override + public SecretsSupplier buildSupplier() { + return new VaultSecretsSupplier(engine, secretsConfig.path() + "/"); + } +} diff --git a/servers/quarkus-server/build.gradle.kts b/servers/quarkus-server/build.gradle.kts index 1ffef1ca560..515aae278b3 100644 --- a/servers/quarkus-server/build.gradle.kts +++ b/servers/quarkus-server/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(project(":nessie-catalog-service-impl")) implementation(project(":nessie-catalog-service-rest")) implementation(project(":nessie-catalog-secrets-api")) + implementation(project(":nessie-catalog-secrets-cache")) implementation(libs.nessie.ui) implementation(enforcedPlatform(libs.quarkus.bom)) @@ -150,6 +151,8 @@ dependencies { testFixturesApi(platform(libs.testcontainers.bom)) testFixturesApi("org.testcontainers:testcontainers") + testFixturesApi("org.testcontainers:localstack") + testFixturesApi("org.testcontainers:vault") testFixturesApi(project(":nessie-keycloak-testcontainer")) testFixturesApi(project(":nessie-azurite-testcontainer")) testFixturesApi(project(":nessie-gcs-testcontainer")) @@ -157,6 +160,9 @@ dependencies { testFixturesApi(project(":nessie-object-storage-mock")) testFixturesApi(project(":nessie-catalog-format-iceberg")) testFixturesApi(project(":nessie-catalog-format-iceberg-fixturegen")) + testFixturesApi(project(":nessie-container-spec-helper")) + + testFixturesApi("io.quarkiverse.vault:quarkus-vault-deployment") testFixturesApi(platform("org.apache.iceberg:iceberg-bom:$versionIceberg")) testFixturesApi("org.apache.iceberg:iceberg-core") @@ -170,12 +176,17 @@ dependencies { testFixturesCompileOnly(libs.microprofile.openapi) + testCompileOnly(project(":nessie-immutables")) + testAnnotationProcessor(project(":nessie-immutables", configuration = "processor")) + intTestImplementation("io.quarkus:quarkus-test-keycloak-server") intTestImplementation(project(":nessie-keycloak-testcontainer")) intTestImplementation(platform(libs.awssdk.bom)) intTestImplementation("software.amazon.awssdk:s3") intTestImplementation("software.amazon.awssdk:sts") + + intTestCompileOnly(libs.immutables.value.annotations) } val pullOpenApiSpec by tasks.registering(Sync::class) diff --git a/servers/quarkus-server/src/main/java/org/projectnessie/server/catalog/CatalogProducers.java b/servers/quarkus-server/src/main/java/org/projectnessie/server/catalog/CatalogProducers.java index 7036f16fe65..75574f2d254 100644 --- a/servers/quarkus-server/src/main/java/org/projectnessie/server/catalog/CatalogProducers.java +++ b/servers/quarkus-server/src/main/java/org/projectnessie/server/catalog/CatalogProducers.java @@ -15,7 +15,9 @@ */ package org.projectnessie.server.catalog; +import static java.lang.String.format; import static java.time.Clock.systemUTC; +import static org.projectnessie.quarkus.providers.RepositoryIdProvider.REPOSITORY_ID_BEAN_NAME; import com.azure.core.http.HttpClient; import com.google.auth.http.HttpTransportFactory; @@ -32,7 +34,6 @@ import jakarta.inject.Named; import jakarta.inject.Singleton; import java.time.Clock; -import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -60,6 +61,10 @@ import org.projectnessie.catalog.files.s3.S3SessionsManager; import org.projectnessie.catalog.files.s3.S3Signer; import org.projectnessie.catalog.secrets.SecretsProvider; +import org.projectnessie.catalog.secrets.cache.CachingSecrets; +import org.projectnessie.catalog.secrets.cache.CachingSecretsBackend; +import org.projectnessie.catalog.secrets.cache.SecretsCacheConfig; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; import org.projectnessie.catalog.service.config.CatalogConfig; import org.projectnessie.client.api.NessieApiV2; import org.projectnessie.nessie.combined.CombinedClientBuilder; @@ -72,7 +77,10 @@ import org.projectnessie.quarkus.config.CatalogGcsConfig; import org.projectnessie.quarkus.config.CatalogS3Config; import org.projectnessie.quarkus.config.CatalogServiceConfig; -import org.projectnessie.quarkus.config.QuarkusCatalogConfig; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; +import org.projectnessie.quarkus.providers.secrets.SecretsSupplierBuilder; +import org.projectnessie.quarkus.providers.secrets.SecretsType; import org.projectnessie.services.rest.RestV2ConfigResource; import org.projectnessie.services.rest.RestV2TreeResource; import org.projectnessie.versioned.storage.common.config.StoreConfig; @@ -155,8 +163,55 @@ public S3CredentialsResolver s3CredentialsResolver(S3Sessions sessions) { @Produces @Singleton - public SecretsProvider secretsProvider(QuarkusCatalogConfig config) { - return new SecretsProvider(names -> Map.of()); + public SecretsSupplier secretsPSupplier( + QuarkusSecretsConfig config, @Any Instance secretsSupplierBuilders) { + SecretsSupplierType type = config.type(); + + if (secretsSupplierBuilders.isUnsatisfied()) { + throw new IllegalStateException("No secrets implementation for " + type); + } + + return secretsSupplierBuilders.select(new SecretsType.Literal(type)).get().buildSupplier(); + } + + @Produces + @Singleton + public SecretsProvider secretsProvider( + @Named(REPOSITORY_ID_BEAN_NAME) String repositoryId, + QuarkusSecretsConfig config, + SecretsSupplier secretsSupplier, + @Any Instance meterRegistry) { + SecretsSupplierType type = config.type(); + + String cacheInfo = ""; + if (type != SecretsSupplierType.NONE && config.cache().enabled()) { + SecretsCacheConfig.Builder cacheConfig = + SecretsCacheConfig.builder() + .clockNanos(System::nanoTime) + .ttlMillis(config.cache().ttl().toMillis()) + .capacityMb(config.cache().capacityMb()); + if (meterRegistry.isResolvable()) { + cacheConfig.meterRegistry(meterRegistry.get()); + } + config.cache().maxElements().ifPresent(cacheConfig::maxElements); + + CachingSecretsBackend backend = new CachingSecretsBackend(cacheConfig.build()); + secretsSupplier = new CachingSecrets(backend).forRepository(repositoryId, secretsSupplier); + + cacheInfo = + format( + ", with capacity of %d MB and TTL of %s", + config.cache().capacityMb(), config.cache().ttl()); + } + + LOGGER.info("Using {} secrets provider{}", type, cacheInfo); + + return new SecretsProvider(secretsSupplier); + } + + public void eagerPersistInitialization( + @Observes StartupEvent event, SecretsProvider secretsProvider) { + // no-op } @Produces diff --git a/servers/quarkus-server/src/main/resources/application.properties b/servers/quarkus-server/src/main/resources/application.properties index 6dda83d9000..322cdf15bb1 100644 --- a/servers/quarkus-server/src/main/resources/application.properties +++ b/servers/quarkus-server/src/main/resources/application.properties @@ -45,6 +45,13 @@ nessie.server.send-stacktrace-to-client=false #quarkus.http.proxy.enable-forwarded-prefix=true #quarkus.http.proxy.trusted-proxies=127.0.0.1 +# Secret managers +nessie.secrets.type=NONE +# When using Google Cloud Secret Manager you may have to configure this to 'true' +quarkus.google.cloud.enable-metadata-server=false +# To enable a specific secrets manager consult the documentations for those, more +# information here: https://projectnessie.org/nessie-latest/configuration/#secrets-manager-settings + ##### Nessie Catalog # Iceberg default config (can be overridden per warehouse) diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/AbstractSecretsSuppliers.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/AbstractSecretsSuppliers.java new file mode 100644 index 00000000000..4da05cac48a --- /dev/null +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/AbstractSecretsSuppliers.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.projectnessie.catalog.secrets.BasicCredentials.basicCredentials; +import static org.projectnessie.catalog.secrets.KeySecret.keySecret; +import static org.projectnessie.catalog.secrets.SecretAttribute.secretAttribute; +import static org.projectnessie.catalog.secrets.TokenSecret.tokenSecret; + +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.projectnessie.catalog.secrets.BasicCredentials; +import org.projectnessie.catalog.secrets.KeySecret; +import org.projectnessie.catalog.secrets.SecretAttribute; +import org.projectnessie.catalog.secrets.SecretType; +import org.projectnessie.catalog.secrets.SecretsProvider; +import org.projectnessie.catalog.secrets.TokenSecret; +import org.projectnessie.catalog.secrets.spi.SecretsSupplier; +import org.projectnessie.nessie.immutables.NessieImmutable; + +public abstract class AbstractSecretsSuppliers { + + // Cannot use @ExtendWith(SoftAssertionsExtension.class) + @InjectSoftAssertions here, because + // of Quarkus class loading issues. See https://github.com/quarkusio/quarkus/issues/19814 + protected final SoftAssertions soft = new SoftAssertions(); + + @SecretsUpdater SecretsUpdateHandler secretsUpdateHandler; + @Inject SecretsSupplier secretsSupplier; + @Inject SecretsProvider secretsProvider; + + @AfterEach + public void tearDown() { + // Cannot use @ExtendWith(SoftAssertionsExtension.class) + @InjectSoftAssertions here, because + // of Quarkus class loading issues. See https://github.com/quarkusio/quarkus/issues/19814 + soft.assertAll(); + } + + @ParameterizedTest + @MethodSource + public void writeAndRead(Map> secrets) { + secretsUpdateHandler.updateSecrets(secrets); + + soft.assertThat(secretsSupplier.resolveSecrets(secrets.keySet())) + .containsExactlyInAnyOrderEntriesOf(secrets); + + secrets.forEach( + (k, v) -> + soft.assertThat(secretsSupplier.resolveSecrets(Set.of(k))) + .containsExactlyEntriesOf(Map.of(k, v))); + } + + static Stream>> writeAndRead() { + return Stream.of( + Map.of("mine", Map.of("foo", "bar")), + Map.of( + // + "one", Map.of("a", "b", "c", "d", "e", "f"), + "two", Map.of("x", "y", "z", "0"))); + } + + @ParameterizedTest + @MethodSource + public void checkProvider(String name, Opts opts) { + TokenSecret tokenSecret = opts.expiringToken().orElseThrow(); + BasicCredentials basicCredentials = opts.basic().orElseThrow(); + KeySecret keySecret = opts.key().orElseThrow(); + + Map> secrets = + Map.of( + "base." + name + ".key", + Map.of(KeySecret.JSON_KEY, keySecret.key()), + "base." + name + ".basic", + Map.of( + BasicCredentials.JSON_NAME, + basicCredentials.name(), + BasicCredentials.JSON_SECRET, + basicCredentials.secret()), + "base." + name + ".expiringToken", + tokenSecret.expiresAt().isEmpty() + ? Map.of(TokenSecret.JSON_TOKEN, tokenSecret.token()) + : Map.of( + TokenSecret.JSON_TOKEN, + tokenSecret.token(), + TokenSecret.JSON_EXPIRES_AT, + tokenSecret.expiresAt().get().toString())); + + secretsUpdateHandler.updateSecrets(secrets); + + soft.assertThat(secretsSupplier.resolveSecrets(secrets.keySet())) + .containsExactlyInAnyOrderEntriesOf(secrets); + + Opts.Builder builder = Opts.builder(); + + Opts retrieved = + secretsProvider + .applySecrets(builder, "base", Opts.EMPTY, name, Opts.EMPTY, providerAttributes()) + .build(); + + soft.assertThat(retrieved.basic()) + .get() + .extracting(BasicCredentials::name, BasicCredentials::secret) + .containsExactly(basicCredentials.name(), basicCredentials.secret()); + soft.assertThat(retrieved.key()).get().extracting(KeySecret::key).isEqualTo(keySecret.key()); + soft.assertThat(retrieved.expiringToken()) + .get() + .extracting(TokenSecret::token, TokenSecret::expiresAt) + .containsExactly(tokenSecret.token(), tokenSecret.expiresAt()); + } + + static List> providerAttributes() { + return List.of( + secretAttribute("key", SecretType.KEY, Opts::key, Opts.Builder::key), + secretAttribute( + "expiringToken", + SecretType.EXPIRING_TOKEN, + Opts::expiringToken, + Opts.Builder::expiringToken), + secretAttribute("basic", SecretType.BASIC, Opts::basic, Opts.Builder::basic)); + } + + static Stream checkProvider() { + String instantStr = "2024-12-24T12:12:12Z"; + Instant instant = Instant.parse(instantStr); + + return Stream.of( + arguments( + "one", + Opts.builder() + .key(keySecret("key")) + .basic(basicCredentials("basic-name", "basic-secret")) + .expiringToken(tokenSecret("exp-token", instant)) + .build()), + arguments( + "two", + Opts.builder() + .key(keySecret("key")) + .basic(basicCredentials("basic-name", "basic-secret")) + .expiringToken(tokenSecret("non-exp-token", null)) + .build())); + } + + @NessieImmutable + public interface Opts { + Opts EMPTY = Opts.builder().build(); + + Optional basic(); + + Optional key(); + + Optional expiringToken(); + + static Builder builder() { + return ImmutableOpts.builder(); + } + + interface Builder { + Builder from(Opts opts); + + Builder basic(BasicCredentials basic); + + Builder key(KeySecret key); + + Builder expiringToken(TokenSecret expiringToken); + + Opts build(); + } + } +} diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsAWS.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsAWS.java new file mode 100644 index 00000000000..7d1801a0e5d --- /dev/null +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsAWS.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource( + restrictToAnnotatedClass = true, + value = LocalstackTestResourceLifecycleManager.class) +public class TestSecretsAWS extends AbstractSecretsSuppliers {} diff --git a/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsVault.java b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsVault.java new file mode 100644 index 00000000000..27c2cfec801 --- /dev/null +++ b/servers/quarkus-server/src/test/java/org/projectnessie/server/secrets/TestSecretsVault.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource( + restrictToAnnotatedClass = true, + value = VaultTestResourceLifecycleManager.class) +public class TestSecretsVault extends AbstractSecretsSuppliers {} diff --git a/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/LocalstackTestResourceLifecycleManager.java b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/LocalstackTestResourceLifecycleManager.java new file mode 100644 index 00000000000..4a39cc9ea61 --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/LocalstackTestResourceLifecycleManager.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SECRETSMANAGER; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.AnnotatedAndMatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URI; +import java.util.Map; +import org.projectnessie.nessie.testing.containerspec.ContainerSpecHelper; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; + +public class LocalstackTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager { + private static final Logger LOGGER = + LoggerFactory.getLogger(LocalstackTestResourceLifecycleManager.class); + + private volatile LocalStackContainer localstack; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface AwsSecretsManagerEndpoint {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface AwsSecretsManagerRegion {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface AwsSecretsManagerAccessKey {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface AwsSecretsManagerSecretAccessKey {} + + @Override + public Map start() { + localstack = + new LocalStackContainer( + ContainerSpecHelper.builder() + .name("localstack") + .containerClass(LocalstackTestResourceLifecycleManager.class) + .build() + .dockerImageName(null) + .asCompatibleSubstituteFor("localstack/localstack")) + .withLogConsumer( + c -> LOGGER.info("[LOCALSTACK] {}", c.getUtf8StringWithoutLineEnding())) + .withServices(SECRETSMANAGER); + + localstack.start(); + + URI secretsManagerEndpoint = localstack.getEndpointOverride(SECRETSMANAGER); + + return ImmutableMap.builder() + .put("quarkus.secretsmanager.endpoint-override", secretsManagerEndpoint.toString()) + .put("quarkus.secretsmanager.aws.region", localstack.getRegion()) + .put("quarkus.secretsmanager.aws.credentials.type", "static") + .put( + "quarkus.secretsmanager.aws.credentials.static-provider.access-key-id", + localstack.getAccessKey()) + .put( + "quarkus.secretsmanager.aws.credentials.static-provider.secret-access-key", + localstack.getSecretKey()) + .put("nessie.secrets.type", SecretsSupplierType.AMAZON.name()) + .build(); + } + + @Override + public void stop() { + if (localstack != null) { + try { + localstack.stop(); + } finally { + localstack = null; + } + } + } + + void updateSecrets(Map> secrets) {} + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields( + localstack.getEndpointOverride(SECRETSMANAGER), + new AnnotatedAndMatchesType(AwsSecretsManagerEndpoint.class, URI.class)); + testInjector.injectIntoFields( + localstack.getRegion(), + new AnnotatedAndMatchesType(AwsSecretsManagerRegion.class, String.class)); + testInjector.injectIntoFields( + localstack.getAccessKey(), + new AnnotatedAndMatchesType(AwsSecretsManagerAccessKey.class, String.class)); + testInjector.injectIntoFields( + localstack.getSecretKey(), + new AnnotatedAndMatchesType(AwsSecretsManagerSecretAccessKey.class, String.class)); + testInjector.injectIntoFields( + (SecretsUpdateHandler) this::updateSecrets, + new AnnotatedAndMatchesType(SecretsUpdater.class, SecretsUpdateHandler.class)); + } +} diff --git a/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdateHandler.java b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdateHandler.java new file mode 100644 index 00000000000..b868d7d86c1 --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdateHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import java.util.Map; + +@FunctionalInterface +public interface SecretsUpdateHandler { + void updateSecrets(Map> secrets); +} diff --git a/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdater.java b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdater.java new file mode 100644 index 00000000000..7cbbfba75eb --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/SecretsUpdater.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SecretsUpdater {} diff --git a/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/VaultTestResourceLifecycleManager.java b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/VaultTestResourceLifecycleManager.java new file mode 100644 index 00000000000..2f0b3a4c978 --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/java/org/projectnessie/server/secrets/VaultTestResourceLifecycleManager.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 Dremio + * + * 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 org.projectnessie.server.secrets; + +import static io.quarkus.vault.client.api.VaultAuthAccessor.DEFAULT_USERPASS_MOUNT_PATH; +import static io.quarkus.vault.client.api.VaultSecretsAccessor.DEFAULT_KV2_MOUNT_PATH; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.AnnotatedAndMatchesType; +import io.quarkus.vault.client.VaultClient; +import io.quarkus.vault.client.api.VaultAuthAccessor; +import io.quarkus.vault.client.api.VaultSecretsAccessor; +import io.quarkus.vault.client.api.VaultSysAccessor; +import io.quarkus.vault.client.api.auth.userpass.VaultAuthUserPassUpdateUserParams; +import io.quarkus.vault.client.api.secrets.kv2.VaultSecretsKV2UpdateSecretOptions; +import io.quarkus.vault.client.http.jdk.JDKVaultHttpClient; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.http.HttpClient; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.projectnessie.nessie.testing.containerspec.ContainerSpecHelper; +import org.projectnessie.quarkus.config.QuarkusSecretsConfig.SecretsSupplierType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; +import org.testcontainers.vault.VaultContainer; + +public class VaultTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager { + private static final Logger LOGGER = + LoggerFactory.getLogger(VaultTestResourceLifecycleManager.class); + + public static final String VAULT_ROOT_TOKEN = "root"; + public static final String VAULT_USERPASS_AUTH_MOUNT = DEFAULT_USERPASS_MOUNT_PATH; + public static final String VAULT_MOUNT = DEFAULT_KV2_MOUNT_PATH; + public static final String NESSIE_SECRETS_PATH = "apps/nessie/secrets"; + public static final String NESSIE_SECRETS_CONFIG = NESSIE_SECRETS_PATH + "/config"; + public static final String VAULT_USERNAME = "nessie_user"; + public static final String VAULT_PASSWORD = "nessie_password"; + public static final String VAULT_POLICY = "nessie-policy"; + + private volatile VaultContainer vaultContainer; + private volatile VaultSecretsAccessor secretsAccessor; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface VaultUri {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface VaultToken {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface VaultMountPath {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface VaultSecretsPath {} + + @SuppressWarnings("resource") + @Override + public Map start() { + vaultContainer = + new VaultContainer<>( + ContainerSpecHelper.builder() + .name("vault") + .containerClass(VaultTestResourceLifecycleManager.class) + .build() + .dockerImageName(null) + .asCompatibleSubstituteFor("vault")) + .withLogConsumer(c -> LOGGER.info("[VAULT] {}", c.getUtf8StringWithoutLineEnding())) + .withVaultToken(VAULT_ROOT_TOKEN) + .withInitCommand("auth enable userpass"); + + vaultContainer.start(); + + try { + VaultClient vaultClient = + VaultClient.builder() + .clientToken(VAULT_ROOT_TOKEN) + .baseUrl(vaultContainer.getHttpHostAddress()) + .executor(new JDKVaultHttpClient(HttpClient.newHttpClient())) + .build(); + + // Following the example at + // https://docs.quarkiverse.io/quarkus-vault/dev/index.html#_starting_vault + + VaultSysAccessor sysAccessor = new VaultSysAccessor(vaultClient); + VaultAuthAccessor authAccessor = new VaultAuthAccessor(vaultClient); + secretsAccessor = new VaultSecretsAccessor(vaultClient); + + secretsAccessor + .kv2(VAULT_MOUNT) + .updateSecret( + NESSIE_SECRETS_CONFIG, + new VaultSecretsKV2UpdateSecretOptions(), + Map.of("some", "thing")) + .toCompletableFuture() + .get(10, SECONDS); + + sysAccessor + .policy() + .update( + VAULT_POLICY, + format( + "path \"%s/data/%s/*\" {\n capabilities = [\"read\", \"create\"]\n}\n", + VAULT_MOUNT, NESSIE_SECRETS_PATH)) + .toCompletableFuture() + .get(10, SECONDS); + + authAccessor + .userPass(VAULT_USERPASS_AUTH_MOUNT) + .updateUser( + VAULT_USERNAME, + new VaultAuthUserPassUpdateUserParams() + .setPassword(VAULT_PASSWORD) + .setTokenPolicies(List.of(VAULT_POLICY))) + .toCompletableFuture() + .get(10, SECONDS); + + return ImmutableMap.builder() + .put("quarkus.vault.url", vaultContainer.getHttpHostAddress()) + .put("quarkus.vault.tls.skip-verify", "true") + .put( + // "quarkus.vault.authentication.client-token", + // VAULT_ROOT_TOKEN, + "quarkus.vault.authentication.userpass.username", VAULT_USERNAME) + .put("quarkus.vault.authentication.userpass.password", VAULT_PASSWORD) + .put("quarkus.vault.authentication.userpass.auth-mount-path", VAULT_USERPASS_AUTH_MOUNT) + .put("quarkus.vault.kv-secret-engine-mount-path", VAULT_MOUNT) + .put("quarkus.vault.secret-config-kv-path", NESSIE_SECRETS_CONFIG) + .put("nessie.secrets.type", SecretsSupplierType.VAULT.name()) + .put("nessie.secrets.path", NESSIE_SECRETS_PATH) + .build(); + } catch (Exception e) { + RuntimeException t = new RuntimeException(e); + try { + stop(); + } catch (Exception ex) { + t.addSuppressed(ex); + } + throw t; + } + } + + @Override + public void stop() { + if (vaultContainer != null) { + try { + vaultContainer.stop(); + } finally { + vaultContainer = null; + } + } + } + + void updateSecrets(Map> secrets) { + CompletableFuture[] futures = + secrets.entrySet().stream() + .map( + e -> { + @SuppressWarnings({"unchecked", "rawtypes"}) + Map value = (Map) (Map) e.getValue(); + return secretsAccessor + .kv2(VAULT_MOUNT) + .updateSecret( + NESSIE_SECRETS_PATH + "/" + e.getKey().replace('.', '/'), + new VaultSecretsKV2UpdateSecretOptions(), + value); + }) + .map(CompletionStage::toCompletableFuture) + .toArray(CompletableFuture[]::new); + try { + CompletableFuture.allOf(futures).get(10, SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields( + vaultContainer.getHttpHostAddress(), + new AnnotatedAndMatchesType(VaultUri.class, String.class)); + testInjector.injectIntoFields( + VAULT_ROOT_TOKEN, new AnnotatedAndMatchesType(VaultToken.class, String.class)); + testInjector.injectIntoFields( + VAULT_MOUNT, new AnnotatedAndMatchesType(VaultMountPath.class, String.class)); + testInjector.injectIntoFields( + NESSIE_SECRETS_PATH, new AnnotatedAndMatchesType(VaultSecretsPath.class, String.class)); + testInjector.injectIntoFields( + (SecretsUpdateHandler) this::updateSecrets, + new AnnotatedAndMatchesType(SecretsUpdater.class, SecretsUpdateHandler.class)); + } +} diff --git a/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-localstack-version b/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-localstack-version new file mode 100644 index 00000000000..5e5015a43ea --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-localstack-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM docker.io/localstack/localstack:3.5.0 diff --git a/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-vault-version b/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-vault-version new file mode 100644 index 00000000000..f019422d362 --- /dev/null +++ b/servers/quarkus-server/src/testFixtures/resources/org/projectnessie/server/secrets/Dockerfile-vault-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM docker.io/hashicorp/vault:1.17.0 diff --git a/site/in-dev/configuration.md b/site/in-dev/configuration.md index 9f4573f4d38..4e12189a359 100644 --- a/site/in-dev/configuration.md +++ b/site/in-dev/configuration.md @@ -141,6 +141,48 @@ Related Quarkus settings: {% include './generated-docs/smallrye-nessie_catalog_service.md' %} +#### Secrets manager settings + +{% include './generated-docs/smallrye-nessie_secrets.md' %} + +!!! info + See the Quarkus/Quarkiverse documentations for [Vault](https://docs.quarkiverse.io/quarkus-vault/dev/index.html), + [Google Cloud Secrets Manager](https://docs.quarkiverse.io/quarkus-google-cloud-services/main/secretmanager.html) and + [Amazon Secrets Manager Client](https://docs.quarkiverse.io/quarkus-amazon-services/dev/amazon-secretsmanager.html) + on how to configure these. + +##### Types of Secrets + +* **Basic credentials** are composites of a `name` attribute and a `secret` attribute. + AWS credentials are managed as basic credentials, where the `name` represents the access key ID and + the `secret` represents the secret access key. +* **Tokens** are composites of a `token` attribute and an optional `expiresAt` attribute, latter + represented as an instant. +* **Keys** consist of a single `key` attribute. + +Secrets are generally stored as JSON objects representing a map of strings to strings, where the map keys +are defined by the type of the secret as mentioned above. + +##### Secrets in Google Cloud Secrets Manager and Amazon Secrets Manager + +In Google Cloud Secrets Manager and Amazon Secrets Manager all secrets are stored as a simple string. + +For example, a basic credential has to be stored as JSON like this: + +```json +{"name": "mysecret", "secret": "mypassword"} +``` + +A token with an expiration date has to be stored as JSON like this: + +```json +{"token": "rkljmnfgoi4jfgoiujh23o4irj", "expiresAt": "2024-06-05T20:38:16Z"} +``` + +##### Secrets in Google Cloud Secrets Manager and Amazon Secrets Manager + +In Vault, secrets are stored as a map of strings to strings, see [above](#types-of-secrets). + ### Version Store Settings {% include './generated-docs/smallrye-nessie_version_store.md' %} diff --git a/tools/server-admin/src/main/resources/application.properties b/tools/server-admin/src/main/resources/application.properties index ac0f1d6211c..db4094a34cc 100644 --- a/tools/server-admin/src/main/resources/application.properties +++ b/tools/server-admin/src/main/resources/application.properties @@ -20,6 +20,23 @@ nessie.server.default-branch=main nessie.server.send-stacktrace-to-client=false +# To provide secrets via a keystore via Quarkus, the following configuration +# options need to be configured accordingly. +# For details see https://quarkus.io/guides/config-secrets#store-secrets-in-a-keystore +#smallrye.config.source.keystore."properties".path=properties +#smallrye.config.source.keystore."properties".password=arealpassword +#smallrye.config.source.keystore."properties".handler=aes-gcm-nopadding +#smallrye.config.source.keystore."key".path=key +#smallrye.config.source.keystore."key".password=anotherpassword + +# Secret managers +nessie.secrets.type=NONE +# When using Google Cloud Secret Manager you may have to configure this to 'true' +quarkus.google.cloud.enable-metadata-server=false +# To enable a specific secrets manager consult the documentations for those, more +# information here: https://projectnessie.org/nessie-latest/configuration/#secrets-manager-settings + + ### which type of version store to use: IN_MEMORY, ROCKSDB, DYNAMODB, MONGODB, CASSANDRA, JDBC, BIGTABLE. # Note: legacy configuration in `nessie.version.store.advanced` is _not_ applied to the version # store types above. Use the config options starting with `nessie.version.store.persist`.