diff --git a/.github/workflows/run-end2end-tests.yml b/.github/workflows/run-end2end-tests.yml index f5179918..a50d33e7 100644 --- a/.github/workflows/run-end2end-tests.yml +++ b/.github/workflows/run-end2end-tests.yml @@ -66,7 +66,7 @@ jobs: ./gradlew clean dockerize - name: "Load Docker Images" run: |- - kind load docker-image --name mxd data-service-api tx-identityhub tx-catalog-server tx-control-plane + kind load docker-image --name mxd data-service-api tx-identityhub tx-identityhub-sts tx-catalog-server tx-sts - name: "Terraform init" working-directory: mxd diff --git a/mxd-runtimes/gradle/libs.versions.toml b/mxd-runtimes/gradle/libs.versions.toml index 3e19dc93..b2a535b7 100644 --- a/mxd-runtimes/gradle/libs.versions.toml +++ b/mxd-runtimes/gradle/libs.versions.toml @@ -51,14 +51,20 @@ edc-lib-keys = { module = "org.eclipse.edc:keys-lib", version.ref = "edc" } edc-lib-transform = { module = "org.eclipse.edc:transform-lib", version.ref = "edc" } edc-lib-jsonld = { module = "org.eclipse.edc:json-ld-lib", version.ref = "edc" } -# identith-hub SPI modules +# identith-hub modules edc-ih-spi = { module = "org.eclipse.edc:identity-hub-spi", version.ref = "edc" } +edc-ih-account-remote = { module = "org.eclipse.edc:sts-account-service-remote", version.ref = "edc" } # identity hub BOM modules bom-ih-withsts = { module = "org.eclipse.edc:identityhub-with-sts-bom", version.ref = "edc" } +bom-ih = { module = "org.eclipse.edc:identityhub-bom", version.ref = "edc" } bom-ih-sql = { module = "org.eclipse.edc:identityhub-feature-sql-bom", version.ref = "edc" } bom-ih-sql-sts = { module = "org.eclipse.edc:identityhub-feature-sql-sts-bom", version.ref = "edc" } +# STS modules +edc-sql-sts-store = { module = "org.eclipse.edc:sts-client-store-sql", version.ref = "edc" } +bom-sts = { module = "org.eclipse.edc:sts-feature-bom", version.ref = "edc" } + # Tractus-X Runtime BOMs edc-tx-controlplane = { module = "org.eclipse.tractusx.edc:edc-controlplane-postgresql-hashicorp-vault", version.ref = "tractusx" } @@ -76,6 +82,7 @@ testcontainers-postgres = { module = "org.testcontainers:postgresql", version.re [bundles] connector = ["edc-boot", "edc-core-connector", "edc-ext-http", "edc-api-observability", "edc-ext-jsonld"] +sql-sts = [ "edc-sql-sts-store", "edc-sql-core", "edc-sql-pool", "edc-sql-transactionlocal", "postgres"] [plugins] diff --git a/mxd-runtimes/settings.gradle.kts b/mxd-runtimes/settings.gradle.kts index 815f5ff9..a3a0e2f2 100644 --- a/mxd-runtimes/settings.gradle.kts +++ b/mxd-runtimes/settings.gradle.kts @@ -43,8 +43,9 @@ dependencyResolutionManagement { } include(":tx-identityhub") -include(":tx-control-plane") +include(":tx-identityhub-sts") include(":tx-catalog-server") +include(":tx-sts") include(":data-service-api") include(":jwt-signer") include(":e2e-test") diff --git a/mxd-runtimes/tx-control-plane/build.gradle.kts b/mxd-runtimes/tx-identityhub-sts/build.gradle.kts similarity index 68% rename from mxd-runtimes/tx-control-plane/build.gradle.kts rename to mxd-runtimes/tx-identityhub-sts/build.gradle.kts index 672f21b5..9d3ae7e2 100644 --- a/mxd-runtimes/tx-control-plane/build.gradle.kts +++ b/mxd-runtimes/tx-identityhub-sts/build.gradle.kts @@ -20,11 +20,18 @@ plugins { dependencies { - runtimeOnly(libs.edc.tx.controlplane) - implementation(libs.edc.spi.crawler) - implementation(catalogLibs.tx.dcp) - implementation(libs.edc.spi.identitytrust) + // used for the runtime + runtimeOnly(libs.bom.ih.withsts) + runtimeOnly(libs.bom.ih.sql) + runtimeOnly(libs.bom.ih.sql.sts) + runtimeOnly(libs.edc.vault.hashicorp) + // used for custom extensions + implementation(libs.edc.core.connector) + implementation(libs.edc.ih.spi) + + testImplementation(libs.edc.lib.crypto) + testImplementation(libs.edc.lib.keys) testImplementation(libs.edc.junit) } diff --git a/mxd-runtimes/tx-control-plane/src/main/docker/Dockerfile b/mxd-runtimes/tx-identityhub-sts/src/main/docker/Dockerfile similarity index 93% rename from mxd-runtimes/tx-control-plane/src/main/docker/Dockerfile rename to mxd-runtimes/tx-identityhub-sts/src/main/docker/Dockerfile index e346443e..ed366e35 100644 --- a/mxd-runtimes/tx-control-plane/src/main/docker/Dockerfile +++ b/mxd-runtimes/tx-identityhub-sts/src/main/docker/Dockerfile @@ -20,7 +20,7 @@ RUN adduser \ USER "$APP_USER" WORKDIR /app -COPY ${JAR} controlplane.jar +COPY ${JAR} identityhub.jar COPY ${OTEL_JAR} opentelemetry-javaagent.jar COPY ${ADDITIONAL_FILES} ./ @@ -34,4 +34,4 @@ ENTRYPOINT ["java", \ "-Dotel.exporter.prometheus.port=9090", \ "-Djava.security.egd=file:/dev/urandom", \ "-jar", \ - "controlplane.jar"] + "identityhub.jar"] diff --git a/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/IdentityHubExtension.java b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/IdentityHubExtension.java new file mode 100644 index 00000000..b0c0e06d --- /dev/null +++ b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/IdentityHubExtension.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.demo; + +import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; + + +@Extension("DCP Demo: Core Extension for IdentityHub") +public class IdentityHubExtension implements ServiceExtension { + + @Provider + public ScopeToCriterionTransformer createScopeTransformer() { + return new TxScopeToCriterionTransformer(); + } + +} diff --git a/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/TxScopeToCriterionTransformer.java b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/TxScopeToCriterionTransformer.java new file mode 100644 index 00000000..252c9572 --- /dev/null +++ b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/demo/TxScopeToCriterionTransformer.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.demo; + +import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.result.Result; + +import java.util.List; + +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; + +/** + * Implementation of {@link ScopeToCriterionTransformer} similar to the upstream one that maps tx scopes + * to {@link Criterion} for querying the credentials (Just for testing) + */ +public class TxScopeToCriterionTransformer implements ScopeToCriterionTransformer { + + public static final String TYPE_OPERAND = "verifiableCredential.credential.type"; + public static final String ALIAS_LITERAL = "org.eclipse.tractusx.vc.type"; + public static final String CONTAINS_OPERATOR = "contains"; + private static final String SCOPE_SEPARATOR = ":"; + private final List allowedOperations = List.of("read", "*", "all"); + + @Override + public Result transform(String scope) { + var tokens = tokenize(scope); + if (tokens.failed()) { + return failure("Scope string cannot be converted: %s".formatted(tokens.getFailureDetail())); + } + var credentialType = tokens.getContent()[1]; + return success(new Criterion(TYPE_OPERAND, CONTAINS_OPERATOR, credentialType)); + } + + protected Result tokenize(String scope) { + if (scope == null) return failure("Scope was null"); + + var tokens = scope.split(SCOPE_SEPARATOR); + if (tokens.length != 3) { + return failure("Scope string has invalid format."); + } + if (!ALIAS_LITERAL.equalsIgnoreCase(tokens[0])) { + return failure("Scope alias MUST be %s but was %s".formatted(ALIAS_LITERAL, tokens[0])); + } + if (!allowedOperations.contains(tokens[2])) { + return failure("Invalid scope operation: " + tokens[2]); + } + + return success(tokens); + } +} diff --git a/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/seed/SuperUserSeedExtension.java b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/seed/SuperUserSeedExtension.java new file mode 100644 index 00000000..98585131 --- /dev/null +++ b/mxd-runtimes/tx-identityhub-sts/src/main/java/org/eclipse/edc/identityhub/seed/SuperUserSeedExtension.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.seed; + +import org.eclipse.edc.identityhub.spi.authentication.ServicePrincipal; +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.util.List; +import java.util.Map; + +import static java.util.Optional.ofNullable; + +public class SuperUserSeedExtension implements ServiceExtension { + public static final String NAME = "MVD ParticipantContext Seed Extension"; + public static final String DEFAULT_SUPER_USER_PARTICIPANT_ID = "super-user"; + + @Setting(value = "Explicitly set the initial API key for the Super-User") + public static final String SUPERUSER_APIKEY_PROPERTY = "edc.ih.api.superuser.key"; + + @Setting(value = "Config value to set the super-user's participant ID.", defaultValue = DEFAULT_SUPER_USER_PARTICIPANT_ID) + public static final String SUPERUSER_PARTICIPANT_ID_PROPERTY = "edc.ih.api.superuser.id"; + private String superUserParticipantId; + private String superUserApiKey; + private Monitor monitor; + @Inject + private ParticipantContextService participantContextService; + @Inject + private Vault vault; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + superUserParticipantId = context.getSetting(SUPERUSER_PARTICIPANT_ID_PROPERTY, DEFAULT_SUPER_USER_PARTICIPANT_ID); + superUserApiKey = context.getSetting(SUPERUSER_APIKEY_PROPERTY, null); + monitor = context.getMonitor(); + } + + @Override + public void start() { + // create super-user + if (participantContextService.getParticipantContext(superUserParticipantId).succeeded()) { // already exists + monitor.debug("super-user already exists with ID '%s', will not re-create".formatted(superUserParticipantId)); + return; + } + participantContextService.createParticipantContext(ParticipantManifest.Builder.newInstance() + .participantId(superUserParticipantId) + .did("did:web:%s".formatted(superUserParticipantId)) // doesn't matter, not intended for resolution + .active(true) + .key(KeyDescriptor.Builder.newInstance() + .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) + .keyId("%s-key".formatted(superUserParticipantId)) + .privateKeyAlias("%s-alias".formatted(superUserParticipantId)) + .build()) + .roles(List.of(ServicePrincipal.ROLE_ADMIN)) + .build()) + .onSuccess(generatedKey -> { + var apiKey = ofNullable(superUserApiKey) + .map(key -> { + if (!key.contains(".")) { + monitor.warning("Super-user key override: this key appears to have an invalid format, you may be unable to access some APIs. It must follow the structure: 'base64().'"); + } + participantContextService.getParticipantContext(superUserParticipantId) + .onSuccess(pc -> vault.storeSecret(pc.getApiTokenAlias(), key) + .onSuccess(u -> monitor.debug("Super-user key override successful")) + .onFailure(f -> monitor.warning("Error storing API key in vault: %s".formatted(f.getFailureDetail())))) + .onFailure(f -> monitor.warning("Error overriding API key for '%s': %s".formatted(superUserParticipantId, f.getFailureDetail()))); + return key; + }) + .orElse(generatedKey.get("apiKey").toString()); + monitor.info("Created user 'super-user'. Please take note of the API Key: %s".formatted(apiKey)); + }) + .orElseThrow(f -> new EdcException("Error creating Super-User: " + f.getFailureDetail())); + } +} diff --git a/mxd-runtimes/tx-identityhub-sts/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/mxd-runtimes/tx-identityhub-sts/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000..9802c276 --- /dev/null +++ b/mxd-runtimes/tx-identityhub-sts/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation +# +# + +org.eclipse.edc.identityhub.demo.IdentityHubExtension +org.eclipse.edc.identityhub.seed.SuperUserSeedExtension \ No newline at end of file diff --git a/mxd-runtimes/tx-identityhub-sts/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java b/mxd-runtimes/tx-identityhub-sts/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java new file mode 100644 index 00000000..100f094e --- /dev/null +++ b/mxd-runtimes/tx-identityhub-sts/src/test/java/org/eclipse/edc/identityhub/seed/ParticipantContextSeedExtensionTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.seed; + +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +class ParticipantContextSeedExtensionTest { + + public static final String SUPER_USER = "super-user"; + private final ParticipantContextService participantContextService = mock(); + private final Vault vault = mock(); + private final Monitor monitor = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(ParticipantContextService.class, participantContextService); + context.registerService(Vault.class, vault); + context.registerService(Monitor.class, monitor); + when(participantContextService.getParticipantContext(eq(SUPER_USER))).thenReturn(ServiceResult.notFound("foobar")); + } + + @Test + void start_verifySuperUser(SuperUserSeedExtension ext, + ServiceExtensionContext context) { + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success(Map.of("apiKey", "some-key"))); + + ext.initialize(context); + + ext.start(); + verify(participantContextService).getParticipantContext(eq(SUPER_USER)); + verify(participantContextService).createParticipantContext(any()); + verifyNoMoreInteractions(participantContextService); + } + + @Test + void start_failsToCreate(SuperUserSeedExtension ext, ServiceExtensionContext context) { + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.badRequest("test-message")); + ext.initialize(context); + assertThatThrownBy(ext::start).isInstanceOf(EdcException.class); + + verify(participantContextService).getParticipantContext(eq(SUPER_USER)); + verify(participantContextService).createParticipantContext(any()); + verifyNoMoreInteractions(participantContextService); + } + + @Test + void start_withApiKeyOverride(SuperUserSeedExtension ext, + ServiceExtensionContext context) { + + + when(vault.storeSecret(any(), any())).thenReturn(Result.success()); + + var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; + when(context.getSetting(eq(SuperUserSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success(Map.of("apiKey", "generated-api-key"))); + when(participantContextService.getParticipantContext(eq(SUPER_USER))) + .thenReturn(ServiceResult.notFound("foobar")) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + ext.start(); + verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); + verify(participantContextService).createParticipantContext(any()); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verifyNoMoreInteractions(participantContextService, vault); + } + + @Test + void start_withInvalidKeyOverride(SuperUserSeedExtension ext, + ServiceExtensionContext context) { + when(vault.storeSecret(any(), any())).thenReturn(Result.success()); + + var apiKeyOverride = "some-invalid-key"; + when(context.getSetting(eq(SuperUserSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success(Map.of("apiKey", "generated-api-key"))); + when(participantContextService.getParticipantContext(eq(SUPER_USER))) + .thenReturn(ServiceResult.notFound("foobar")) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + ext.start(); + verify(participantContextService).createParticipantContext(any()); + verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verify(monitor).warning(contains("this key appears to have an invalid format")); + verifyNoMoreInteractions(participantContextService, vault); + } + + @Test + void start_whenVaultReturnsFailure(SuperUserSeedExtension ext, + ServiceExtensionContext context) { + when(vault.storeSecret(any(), any())).thenReturn(Result.failure("test-failure")); + + var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; + when(context.getSetting(eq(SuperUserSeedExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success(Map.of("apiKey", "generated-api-key"))); + when(participantContextService.getParticipantContext(eq(SUPER_USER))) + .thenReturn(ServiceResult.notFound("foobar")) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + ext.start(); + verify(participantContextService, times(2)).getParticipantContext(eq(SUPER_USER)); + verify(participantContextService).createParticipantContext(any()); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verify(monitor).warning(eq("Error storing API key in vault: test-failure")); + verifyNoMoreInteractions(participantContextService, vault); + } + + private ParticipantContext.Builder superUserContext() { + return ParticipantContext.Builder.newInstance() + .participantId(SUPER_USER) + .apiTokenAlias("super-user-apikey"); + + } +} \ No newline at end of file diff --git a/mxd-runtimes/tx-identityhub/build.gradle.kts b/mxd-runtimes/tx-identityhub/build.gradle.kts index 9d3ae7e2..108a159c 100644 --- a/mxd-runtimes/tx-identityhub/build.gradle.kts +++ b/mxd-runtimes/tx-identityhub/build.gradle.kts @@ -21,9 +21,9 @@ plugins { dependencies { // used for the runtime - runtimeOnly(libs.bom.ih.withsts) + runtimeOnly(libs.bom.ih) runtimeOnly(libs.bom.ih.sql) - runtimeOnly(libs.bom.ih.sql.sts) + runtimeOnly(libs.edc.ih.account.remote) runtimeOnly(libs.edc.vault.hashicorp) // used for custom extensions diff --git a/mxd-runtimes/tx-sts/build.gradle.kts b/mxd-runtimes/tx-sts/build.gradle.kts new file mode 100644 index 00000000..c34304b7 --- /dev/null +++ b/mxd-runtimes/tx-sts/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +plugins { + `java-library` + id("application") + alias(libs.plugins.shadow) +} + + +dependencies { + runtimeOnly(libs.bom.sts) + runtimeOnly(libs.edc.vault.hashicorp) + runtimeOnly(libs.bundles.sql.sts) +} + +tasks.withType { + exclude("**/pom.properties", "**/pom.xml") + mergeServiceFiles() + archiveFileName.set("${project.name}.jar") +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +edcBuild { + publish.set(false) +} \ No newline at end of file diff --git a/mxd-runtimes/tx-sts/src/main/docker/Dockerfile b/mxd-runtimes/tx-sts/src/main/docker/Dockerfile new file mode 100644 index 00000000..54591684 --- /dev/null +++ b/mxd-runtimes/tx-sts/src/main/docker/Dockerfile @@ -0,0 +1,37 @@ +FROM eclipse-temurin:23_37-jre-alpine +ARG JAR +ARG OTEL_JAR +ARG ADDITIONAL_FILES + +ARG APP_USER=docker +ARG APP_UID=10100 + +RUN addgroup --system "$APP_USER" + +RUN adduser \ + --shell /sbin/nologin \ + --disabled-password \ + --gecos "" \ + --ingroup "$APP_USER" \ + --no-create-home \ + --uid "$APP_UID" \ + "$APP_USER" + +USER "$APP_USER" +WORKDIR /app + +COPY ${JAR} sts.jar +COPY ${OTEL_JAR} opentelemetry-javaagent.jar +COPY ${ADDITIONAL_FILES} ./ + +HEALTHCHECK NONE + +ENTRYPOINT ["java", \ + "-javaagent:/app/opentelemetry-javaagent.jar", \ + "-Dedc.fs.config=/app/configuration.properties", \ + "-Dotel.javaagent.configuration-file=/app/opentelemetry.properties", \ + "-Dotel.metrics.exporter=prometheus", \ + "-Dotel.exporter.prometheus.port=9090", \ + "-Djava.security.egd=file:/dev/urandom", \ + "-jar", \ + "sts.jar"] diff --git a/mxd/alice.tf b/mxd/alice.tf index 9dfd9aab..9f95267f 100644 --- a/mxd/alice.tf +++ b/mxd/alice.tf @@ -26,7 +26,7 @@ module "alice-connector" { } dcp-config = { id = var.alice-did - sts_token_url = "http://${var.alice-identityhub-host}:7084/api/credentials/token" + sts_token_url = local.sts-token-url sts_client_id = var.alice-did sts_clientsecret_alias = "participant-alice-sts-client-secret" } @@ -61,8 +61,25 @@ module "alice-identityhub" { humanReadableName = var.alice-identityhub-host namespace = kubernetes_namespace.mxd-ns.metadata.0.name participantId = var.alice-did - vault-url = "http://alice-vault:8200" + vault-url = local.vault-url url-path = var.alice-identityhub-host + sts_token_url = local.sts-token-url + sts_accounts_url = local.sts-accounts-url + image = "tx-identityhub:latest" # the one without the STS, which is deployed standalone +} + +module "alice-sts" { + source = "./modules/sts" + humanReadableName = "alice-sts" + accounts-api-key = "password" + namespace = kubernetes_namespace.mxd-ns.metadata.0.name + vault-url = local.vault-url + + database = { + user = local.databases.alice.database-username + password = local.databases.alice.database-password + url = "jdbc:postgresql://${local.alice-postgres.database-host}/${local.databases.alice.database-name}" + } } # alice's catalog server @@ -74,7 +91,7 @@ module "alice-catalog-server" { serviceName = var.alice-catalogserver-host namespace = kubernetes_namespace.mxd-ns.metadata.0.name participantId = var.alice-bpn - vault-url = "http://alice-vault:8200" + vault-url = local.vault-url bdrs-url = "http://bdrs-server:8082/api/directory" database = { user = local.databases.alice-catalogserver.database-username @@ -83,12 +100,13 @@ module "alice-catalog-server" { } dcp-config = { id = var.alice-did - sts_token_url = "http://${var.alice-identityhub-host}:7084/api/credentials/token" + sts_token_url = local.sts-accounts-url sts_client_id = var.alice-did sts_clientsecret_alias = "participant-alice-sts-client-secret" } } + module "alice-minio" { source = "./modules/minio" humanReadableName = lower(var.alice-humanReadableName) @@ -98,4 +116,7 @@ module "alice-minio" { locals { alice-azure-key-base64 = base64encode(var.alice-azure-account-key) + sts-accounts-url = module.alice-sts.account-url + sts-token-url = module.alice-sts.token-url + vault-url = "http://alice-vault:8200" } \ No newline at end of file diff --git a/mxd/assets/participants.json b/mxd/assets/participants.json index 1fc2953c..97a6e210 100644 --- a/mxd/assets/participants.json +++ b/mxd/assets/participants.json @@ -6,9 +6,9 @@ "supportedProtocols": "dataspace-protocol-http" }, { - "name": "alice-cs", + "name": "alice", "id": "BPNL000000000001", - "url": "http://alice-cs:8082/api/dsp", + "url": "http://alice-controlplane:8084/api/v1/dsp", "supportedProtocols": "dataspace-protocol-http" } ] \ No newline at end of file diff --git a/mxd/modules/catalog-server/catalog-server.tf b/mxd/modules/catalog-server/catalog-server.tf index 76b4d179..416735ec 100644 --- a/mxd/modules/catalog-server/catalog-server.tf +++ b/mxd/modules/catalog-server/catalog-server.tf @@ -115,7 +115,6 @@ resource "kubernetes_config_map" "catalog-server-config" { namespace = var.namespace } - ## Create databases for keycloak and MIW, create users and assign privileges data = { EDC_IAM_ISSUER_ID = var.dcp-config.id EDC_IAM_DID_WEB_USE_HTTPS = false diff --git a/mxd/modules/connector/values.yaml b/mxd/modules/connector/values.yaml index da7274bb..48401e24 100644 --- a/mxd/modules/connector/values.yaml +++ b/mxd/modules/connector/values.yaml @@ -40,10 +40,9 @@ controlplane: endpoints: management: authKey: password +# uncomment the following lines if a custom tx-control-plane runtime images should be used. image: - repository: "tx-control-plane" - tag: "latest" - pullPolicy: Never + pullPolicy: Always securityContext: # avoids some errors in the log: cannot write temp files of large multipart requests when R/O diff --git a/mxd/modules/identity-hub/identityhub.tf b/mxd/modules/identity-hub/identityhub.tf index fd343f7d..c40d61eb 100644 --- a/mxd/modules/identity-hub/identityhub.tf +++ b/mxd/modules/identity-hub/identityhub.tf @@ -38,7 +38,7 @@ resource "kubernetes_deployment" "identityhub" { spec { container { image_pull_policy = "Never" - image = "tx-identityhub:latest" + image = var.image name = "tx-identityhub" env_from { @@ -116,33 +116,36 @@ resource "kubernetes_config_map" "identityhub-config" { data = { # IdentityHub variables - EDC_API_AUTH_KEY = "password" - EDC_IH_IAM_ID = var.participantId - EDC_IAM_DID_WEB_USE_HTTPS = false - EDC_IH_IAM_PUBLICKEY_ALIAS = local.public-key-alias - EDC_IH_API_SUPERUSER_KEY = var.ih_superuser_apikey - WEB_HTTP_PORT = var.ports.web - WEB_HTTP_PATH = "/api" - WEB_HTTP_IDENTITY_PORT = var.ports.ih-identity-api - WEB_HTTP_IDENTITY_PATH = "/api/identity" - WEB_HTTP_PRESENTATION_PORT = var.ports.presentation-api - WEB_HTTP_PRESENTATION_PATH = "/api/presentation" - WEB_HTTP_STS_PORT = var.ports.ih-sts - WEB_HTTP_STS_PATH = "/api/credentials" - WEB_HTTP_DID_PORT = var.ports.ih-did - WEB_HTTP_DID_PATH = "/" - JAVA_TOOL_OPTIONS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.ih-debug}" - EDC_IAM_STS_PRIVATEKEY_ALIAS = var.aliases.sts-private-key - EDC_IAM_STS_PUBLICKEY_ID = var.aliases.sts-public-key-id - EDC_MVD_CREDENTIALS_PATH = "/etc/credentials/" - EDC_VAULT_HASHICORP_URL = var.vault-url - EDC_VAULT_HASHICORP_TOKEN = var.vault-token - EDC_DATASOURCE_DEFAULT_URL = var.database.url - EDC_DATASOURCE_DEFAULT_USER = var.database.user - EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password - EDC_SQL_SCHEMA_AUTOCREATE = true - EDC_IAM_DID_WEB_USE_HTTPS = "false" - EDC_API_ACCOUNTS_KEY = "password" + EDC_API_AUTH_KEY = "password" + EDC_IH_IAM_ID = var.participantId + EDC_IAM_DID_WEB_USE_HTTPS = false + EDC_IH_IAM_PUBLICKEY_ALIAS = local.public-key-alias + EDC_IH_API_SUPERUSER_KEY = var.ih_superuser_apikey + WEB_HTTP_PORT = var.ports.web + WEB_HTTP_PATH = "/api" + WEB_HTTP_IDENTITY_PORT = var.ports.ih-identity-api + WEB_HTTP_IDENTITY_PATH = "/api/identity" + WEB_HTTP_PRESENTATION_PORT = var.ports.presentation-api + WEB_HTTP_PRESENTATION_PATH = "/api/presentation" + WEB_HTTP_STS_PORT = var.ports.ih-sts + WEB_HTTP_STS_PATH = "/api/credentials" + WEB_HTTP_DID_PORT = var.ports.ih-did + WEB_HTTP_DID_PATH = "/" + JAVA_TOOL_OPTIONS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${var.ports.ih-debug}" + EDC_IAM_STS_PRIVATEKEY_ALIAS = var.aliases.sts-private-key + EDC_IAM_STS_PUBLICKEY_ID = var.aliases.sts-public-key-id + EDC_MVD_CREDENTIALS_PATH = "/etc/credentials/" + EDC_VAULT_HASHICORP_URL = var.vault-url + EDC_VAULT_HASHICORP_TOKEN = var.vault-token + EDC_DATASOURCE_DEFAULT_URL = var.database.url + EDC_DATASOURCE_DEFAULT_USER = var.database.user + EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password + EDC_SQL_SCHEMA_AUTOCREATE = true + EDC_IAM_DID_WEB_USE_HTTPS = "false" + EDC_STS_ACCOUNT_API_URL = var.sts_accounts_url + EDC_STS_ACCOUNTS_API_AUTH_HEADER_VALUE = var.sts_accounts_api_key + // only relevant when the STS is not embedded + EDC_API_ACCOUNTS_KEY = "password" } } diff --git a/mxd/modules/identity-hub/variables.tf b/mxd/modules/identity-hub/variables.tf index 97bf1b20..ad327a21 100644 --- a/mxd/modules/identity-hub/variables.tf +++ b/mxd/modules/identity-hub/variables.tf @@ -97,4 +97,25 @@ variable "database" { user = string password = string }) +} + +variable "image" { + type = string + default = "tx-identityhub-sts:latest" +} + +variable "sts_token_url" { + default = "" + type = string +} + +variable "sts_accounts_url" { + default = "" + type = string +} + +variable "sts_accounts_api_key" { + type = string + default = "password" + description = "API Key for the STS Accounts API of a standalone STS" } \ No newline at end of file diff --git a/mxd/modules/sts/outputs.tf b/mxd/modules/sts/outputs.tf new file mode 100644 index 00000000..e6643d61 --- /dev/null +++ b/mxd/modules/sts/outputs.tf @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + + +output "token-url" { + value = "http://${var.humanReadableName}:${var.ports.sts}${var.paths.sts}/token" +} + +output "account-url" { + value = "http://${var.humanReadableName}:${var.ports.accounts}${var.paths.accounts}" +} \ No newline at end of file diff --git a/mxd/modules/sts/services.tf b/mxd/modules/sts/services.tf new file mode 100644 index 00000000..bf38622b --- /dev/null +++ b/mxd/modules/sts/services.tf @@ -0,0 +1,38 @@ +# +# Copyright (c) 2024 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + +resource "kubernetes_service" "sts-service" { + metadata { + name = var.humanReadableName + namespace = var.namespace + } + spec { + type = "NodePort" + selector = { + App = kubernetes_deployment.sts.spec.0.template.0.metadata[0].labels.App + } + # we need a stable IP, otherwise there will be a cycle with the issuer + port { + name = "accounts" + port = var.ports.accounts + } + port { + name = "sts" + port = var.ports.sts + } + port { + name = "debug" + port = var.ports.debug + } + } +} \ No newline at end of file diff --git a/mxd/modules/sts/sts.tf b/mxd/modules/sts/sts.tf new file mode 100644 index 00000000..a5498eae --- /dev/null +++ b/mxd/modules/sts/sts.tf @@ -0,0 +1,124 @@ +# +# Copyright (c) 2024 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + +resource "kubernetes_deployment" "sts" { + metadata { + name = lower(var.humanReadableName) + namespace = var.namespace + labels = { + App = lower(var.humanReadableName) + } + } + + spec { + replicas = 1 + selector { + match_labels = { + App = lower(var.humanReadableName) + } + } + + template { + metadata { + labels = { + App = lower(var.humanReadableName) + } + } + + spec { + container { + image_pull_policy = "Never" + image = "tx-sts:latest" + name = "tx-sts" + + env_from { + config_map_ref { + name = kubernetes_config_map.sts-config.metadata[0].name + } + } + port { + container_port = var.ports.accounts + name = "accounts-port" + } + + port { + container_port = var.ports.debug + name = "debug" + } + port { + container_port = var.ports.web + name = "default-port" + } + port { + container_port = var.ports.sts + name = "sts-port" + } + + liveness_probe { + http_get { + port = var.ports.web + path = "/api/check/liveness" + } + failure_threshold = 10 + period_seconds = 5 + timeout_seconds = 30 + } + + readiness_probe { + http_get { + port = var.ports.web + path = "/api/check/readiness" + } + failure_threshold = 10 + period_seconds = 5 + timeout_seconds = 30 + } + + startup_probe { + http_get { + port = var.ports.web + path = "/api/check/startup" + } + failure_threshold = 10 + period_seconds = 5 + timeout_seconds = 30 + } + } + } + } + } +} + +resource "kubernetes_config_map" "sts-config" { + metadata { + name = "${lower(var.humanReadableName)}-config" + namespace = var.namespace + } + + data = { + # STS variables + EDC_API_ACCOUNTS_KEY = var.accounts-api-key + WEB_HTTP_ACCOUNTS_PORT = var.ports.accounts + WEB_HTTP_ACCOUNTS_PATH = var.paths.accounts + WEB_HTTP_STS_PORT = var.ports.sts + WEB_HTTP_STS_PATH = var.paths.sts + WEB_HTTP_PORT = var.ports.web + WEB_HTTP_PATH = var.paths.web + EDC_DATASOURCE_DEFAULT_URL = var.database.url + EDC_DATASOURCE_DEFAULT_USER = var.database.user + EDC_DATASOURCE_DEFAULT_PASSWORD = var.database.password + EDC_SQL_SCHEMA_AUTOCREATE = true + EDC_VAULT_HASHICORP_URL = var.vault-url + EDC_VAULT_HASHICORP_TOKEN = var.vault-token + } +} \ No newline at end of file diff --git a/mxd/modules/sts/variables.tf b/mxd/modules/sts/variables.tf new file mode 100644 index 00000000..d78712f4 --- /dev/null +++ b/mxd/modules/sts/variables.tf @@ -0,0 +1,73 @@ +# +# Copyright (c) 2024 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + + +variable "ports" { + type = object({ + web = number + accounts = number + sts = number + debug = number + }) + default = { + web = 8080 + accounts = 8081 + sts = 8082 + debug = 1044 + } +} + +variable "paths" { + type = object({ + web = string + accounts = string + sts = string + }) + default = { + web = "/api" + accounts = "/api/sts/accounts" + sts = "/api/sts" + } +} + +variable "accounts-api-key" { + type = string + description = "static API key for the STS Accounts API" +} + +variable "namespace" { + type = string +} + +variable "humanReadableName" { + type = string +} + +variable "database" { + type = object({ + url = string + user = string + password = string + }) +} + +variable "vault-url" { + description = "URL of the Hashicorp Vault" + type = string +} + +variable "vault-token" { + default = "root" + description = "This is the authentication token for the vault. DO NOT USE THIS IN PRODUCTION!" + type = string +} \ No newline at end of file