diff --git a/DEPENDENCIES b/DEPENDENCIES index 6f8d0d495e5..793bb0c1f24 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -81,7 +81,7 @@ maven/mavencentral/com.jcraft/jzlib/1.1.3, BSD-2-Clause, approved, CQ6218 maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.41.1, , restricted, clearlydefined +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.41.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.1, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #16060 maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-api/src/test/java/org/eclipse/edc/api/iam/identitytrust/sts/controller/SecureServiceTokenApiControllerTest.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-api/src/test/java/org/eclipse/edc/api/iam/identitytrust/sts/controller/SecureServiceTokenApiControllerTest.java index 0a0d5aee47e..4f17a841297 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-api/src/test/java/org/eclipse/edc/api/iam/identitytrust/sts/controller/SecureServiceTokenApiControllerTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-api/src/test/java/org/eclipse/edc/api/iam/identitytrust/sts/controller/SecureServiceTokenApiControllerTest.java @@ -55,14 +55,17 @@ void token() { var did = "did:example:subject"; var audience = "audience"; var token = "token"; + var publicKeyReference = "publicKeyReference"; + var name = "Name"; var expiresIn = 3600; var client = StsClient.Builder.newInstance() .id(id) .clientId(clientId) - .name("Name") + .name(name) .secretAlias(clientKeyAlias) .privateKeyAlias(privateKeyAlias) + .publicKeyReference(publicKeyReference) .did(did) .build(); diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-client-configuration/src/test/java/org/eclipse/edc/iam/identitytrust/sts/client/configuration/StsClientConfigurationExtensionTest.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-client-configuration/src/test/java/org/eclipse/edc/iam/identitytrust/sts/client/configuration/StsClientConfigurationExtensionTest.java index 55fd2087724..11131dd5d6c 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-client-configuration/src/test/java/org/eclipse/edc/iam/identitytrust/sts/client/configuration/StsClientConfigurationExtensionTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-client-configuration/src/test/java/org/eclipse/edc/iam/identitytrust/sts/client/configuration/StsClientConfigurationExtensionTest.java @@ -32,6 +32,7 @@ import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CLIENT_ID; import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CLIENT_NAME; import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CLIENT_PRIVATE_KEY_ALIAS; +import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CLIENT_PUBLIC_KEY_REFERENCE; import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CLIENT_SECRET_ALIAS; import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.CONFIG_PREFIX; import static org.eclipse.edc.iam.identitytrust.sts.client.configuration.StsClientConfigurationExtension.ID; @@ -65,6 +66,7 @@ void initialize_withClient(ServiceExtensionContext context, StsClientConfigurati .privateKeyAlias("pAlias") .secretAlias("sAlias") .did("did:example:subject") + .publicKeyReference("publicReference") .build(); var clientAlias = "client"; var config = ConfigFactory.fromMap(clientConfig(client, clientAlias)); @@ -74,7 +76,9 @@ void initialize_withClient(ServiceExtensionContext context, StsClientConfigurati var capture = ArgumentCaptor.forClass(StsClient.class); verify(clientStore).create(capture.capture()); - assertThat(capture.getValue()).usingRecursiveComparison().isEqualTo(client); + assertThat(capture.getValue()).usingRecursiveComparison() + .ignoringFields("createdAt") + .isEqualTo(client); } private Map clientConfig(StsClient client, String clientAlias) { @@ -84,7 +88,8 @@ private Map clientConfig(StsClient client, String clientAlias) { clientAlias + "." + CLIENT_ID, client.getClientId(), clientAlias + "." + CLIENT_SECRET_ALIAS, client.getSecretAlias(), clientAlias + "." + CLIENT_DID, client.getDid(), - clientAlias + "." + CLIENT_PRIVATE_KEY_ALIAS, client.getPrivateKeyAlias() + clientAlias + "." + CLIENT_PRIVATE_KEY_ALIAS, client.getPrivateKeyAlias(), + clientAlias + "." + CLIENT_PUBLIC_KEY_REFERENCE, client.getPublicKeyReference() ); } diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/build.gradle.kts b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/build.gradle.kts index 341e388b8b0..7672f2364ca 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/build.gradle.kts +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/build.gradle.kts @@ -11,11 +11,13 @@ dependencies { implementation(project(":spi:common:keys-spi")) implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-embedded")) implementation(project(":core:common:token-core")) + implementation(project(":core:common:lib:store-lib")) testImplementation(testFixtures(project(":spi:common:identity-trust-sts-spi"))) testImplementation(project(":core:common:lib:boot-lib")) testImplementation(project(":core:common:lib:crypto-common-lib")) testImplementation(project(":core:common:lib:keys-lib")) testImplementation(project(":core:common:junit")) + testImplementation(project(":core:common:lib:query-lib")) testImplementation(libs.nimbus.jwt) } diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsDefaultStoresExtension.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsDefaultStoresExtension.java index c19617b05eb..14d7b3308f3 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsDefaultStoresExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsDefaultStoresExtension.java @@ -17,7 +17,9 @@ import org.eclipse.edc.iam.identitytrust.sts.defaults.store.InMemoryStsClientStore; import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; import org.eclipse.edc.spi.system.ServiceExtension; @Extension(StsDefaultStoresExtension.NAME) @@ -25,6 +27,9 @@ public class StsDefaultStoresExtension implements ServiceExtension { public static final String NAME = "Secure Token Service Default Stores"; + @Inject + private CriterionOperatorRegistry criterionOperatorRegistry; + @Override public String name() { return NAME; @@ -32,6 +37,6 @@ public String name() { @Provider(isDefault = true) public StsClientStore clientStore() { - return new InMemoryStsClientStore(); + return new InMemoryStsClientStore(criterionOperatorRegistry); } } diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStore.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStore.java index 79387862e04..41e1f0f013c 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStore.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/main/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStore.java @@ -16,11 +16,17 @@ import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.store.ReflectionBasedQueryResolver; +import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import static java.lang.String.format; @@ -29,19 +35,54 @@ */ public class InMemoryStsClientStore implements StsClientStore { + // we store it by clientId private final Map clients = new ConcurrentHashMap<>(); + private final QueryResolver queryResolver; + + + public InMemoryStsClientStore(CriterionOperatorRegistry criterionOperatorRegistry) { + queryResolver = new ReflectionBasedQueryResolver<>(StsClient.class, criterionOperatorRegistry); + } @Override public StoreResult create(StsClient client) { return Optional.ofNullable(clients.putIfAbsent(client.getClientId(), client)) - .map(old -> StoreResult.alreadyExists(format("Client with id %s already exists", client.getClientId()))) + .map(old -> StoreResult.alreadyExists(format(CLIENT_EXISTS_TEMPLATE, client.getClientId()))) .orElseGet(() -> StoreResult.success(client)); } @Override - public StoreResult findByClientId(String id) { - return Optional.ofNullable(clients.get(id)) + public StoreResult update(StsClient stsClient) { + var prev = clients.replace(stsClient.getClientId(), stsClient); + return Optional.ofNullable(prev) + .map(a -> StoreResult.success()) + .orElse(StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, stsClient.getId()))); + } + + @Override + public @NotNull Stream findAll(QuerySpec spec) { + return queryResolver.query(clients.values().stream(), spec); + } + + @Override + public StoreResult findById(String id) { + return clients.values().stream() + .filter(client -> client.getId().equals(id)) + .findFirst() + .map(StoreResult::success) + .orElseGet(() -> StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, id))); + } + + @Override + public StoreResult findByClientId(String clientId) { + return Optional.ofNullable(clients.get(clientId)) .map(StoreResult::success) - .orElseGet(() -> StoreResult.notFound(format("Client with id %s not found.", id))); + .orElseGet(() -> StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_CLIENT_ID_TEMPLATE, clientId))); + } + + @Override + public StoreResult deleteById(String id) { + return findById(id) + .onSuccess(client -> clients.remove(client.getClientId())); } } diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsClientTokenIssuanceIntegrationTest.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsClientTokenIssuanceIntegrationTest.java index ca22441d14e..8a50c244961 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsClientTokenIssuanceIntegrationTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/StsClientTokenIssuanceIntegrationTest.java @@ -29,6 +29,7 @@ import org.eclipse.edc.keys.keyparsers.PemParser; import org.eclipse.edc.keys.spi.KeyParserRegistry; import org.eclipse.edc.keys.spi.PrivateKeyResolver; +import org.eclipse.edc.query.CriterionOperatorRegistryImpl; import org.eclipse.edc.security.token.jwt.DefaultJwsSignerProvider; import org.eclipse.edc.spi.security.Vault; import org.eclipse.edc.token.JwtGenerationService; @@ -56,7 +57,7 @@ @ComponentTest public class StsClientTokenIssuanceIntegrationTest { - private final InMemoryStsClientStore clientStore = new InMemoryStsClientStore(); + private final InMemoryStsClientStore clientStore = new InMemoryStsClientStore(CriterionOperatorRegistryImpl.ofDefaults()); private final Vault vault = new InMemoryVault(mock()); private final KeyParserRegistry keyParserRegistry = new KeyParserRegistryImpl(); private StsClientServiceImpl clientService; diff --git a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStoreTest.java b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStoreTest.java index e63b1e0e7ea..7401194a258 100644 --- a/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStoreTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-sts/identity-trust-sts-core/src/test/java/org/eclipse/edc/iam/identitytrust/sts/defaults/store/InMemoryStsClientStoreTest.java @@ -16,6 +16,7 @@ import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; import org.eclipse.edc.iam.identitytrust.sts.spi.store.fixtures.StsClientStoreTestBase; +import org.eclipse.edc.query.CriterionOperatorRegistryImpl; import org.junit.jupiter.api.BeforeEach; public class InMemoryStsClientStoreTest extends StsClientStoreTestBase { @@ -24,7 +25,7 @@ public class InMemoryStsClientStoreTest extends StsClientStoreTestBase { @BeforeEach void setUp() { - store = new InMemoryStsClientStore(); + store = new InMemoryStsClientStore(CriterionOperatorRegistryImpl.ofDefaults()); } @Override diff --git a/extensions/common/store/sql/sts-client-store-sql/build.gradle.kts b/extensions/common/store/sql/sts-client-store-sql/build.gradle.kts new file mode 100644 index 00000000000..b65fd58bb00 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/build.gradle.kts @@ -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 + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":spi:common:core-spi")) + api(project(":spi:common:transaction-spi")) + + implementation(project(":extensions:common:sql:sql-core")) + implementation(project(":extensions:common:sql:sql-bootstrapper")) + implementation(project(":spi:common:identity-trust-sts-spi")) + implementation(project(":spi:common:transaction-datasource-spi")) + testImplementation(project(":core:common:junit")) + testImplementation(testFixtures(project(":extensions:common:sql:sql-core"))) + testImplementation(testFixtures(project(":spi:common:identity-trust-sts-spi"))) + +} \ No newline at end of file diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStore.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStore.java new file mode 100644 index 00000000000..5f38fb0337f --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStore.java @@ -0,0 +1,188 @@ +/* + * 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.iam.identitytrust.sts.store; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.StsClientStatements; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.store.AbstractSqlStore; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.lang.String.format; + +public class SqlStsClientStore extends AbstractSqlStore implements StsClientStore { + + private final StsClientStatements statements; + + public SqlStsClientStore(DataSourceRegistry dataSourceRegistry, String dataSourceName, TransactionContext transactionContext, + ObjectMapper objectMapper, StsClientStatements statements, QueryExecutor queryExecutor) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.statements = statements; + } + + @Override + public StoreResult create(StsClient client) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(connection, client.getId()) != null) { + var msg = format(CLIENT_EXISTS_TEMPLATE, client.getId()); + return StoreResult.alreadyExists(msg); + } + + queryExecutor.execute(connection, statements.getInsertTemplate(), + client.getId(), + client.getName(), + client.getClientId(), + client.getDid(), + client.getSecretAlias(), + client.getPrivateKeyAlias(), + client.getPublicKeyReference(), + client.getCreatedAt() + ); + + return StoreResult.success(); + } catch (Exception e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult update(StsClient stsClient) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(connection, stsClient.getId()) != null) { + updateInternal(connection, stsClient); + return StoreResult.success(); + } else { + return StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, stsClient.getId())); + } + } catch (Exception e) { + throw new EdcPersistenceException(e.getMessage(), e); + } + }); + } + + @Override + public @NotNull Stream findAll(QuerySpec spec) { + return transactionContext.execute(() -> { + Objects.requireNonNull(spec); + + try { + var queryStmt = statements.createQuery(spec); + return queryExecutor.query(getConnection(), true, this::mapResultSet, queryStmt.getQueryAsString(), queryStmt.getParameters()); + } catch (SQLException exception) { + throw new EdcPersistenceException(exception); + } + }); + } + + @Override + public StoreResult findById(String id) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var client = findById(connection, id); + if (client == null) { + return StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, id)); + } + return StoreResult.success(client); + } catch (Exception e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult findByClientId(String clientId) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var client = findByClientIdId(connection, clientId); + if (client == null) { + return StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_CLIENT_ID_TEMPLATE, clientId)); + } + return StoreResult.success(client); + } catch (Exception e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult deleteById(String id) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var entity = findById(connection, id); + if (entity != null) { + queryExecutor.execute(connection, statements.getDeleteByIdTemplate(), id); + return StoreResult.success(entity); + } else { + return StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, id)); + } + + } catch (Exception e) { + throw new EdcPersistenceException(e.getMessage(), e); + } + }); + } + + private void updateInternal(Connection connection, StsClient stsClient) { + queryExecutor.execute(connection, statements.getUpdateTemplate(), + stsClient.getId(), + stsClient.getName(), + stsClient.getClientId(), + stsClient.getDid(), + stsClient.getSecretAlias(), + stsClient.getPrivateKeyAlias(), + stsClient.getPublicKeyReference(), + stsClient.getCreatedAt(), + stsClient.getId()); + } + + private StsClient findById(Connection connection, String id) { + var sql = statements.getFindByTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, sql, id); + } + + private StsClient findByClientIdId(Connection connection, String id) { + var sql = statements.getFindByClientIdTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, sql, id); + } + + private StsClient mapResultSet(ResultSet resultSet) throws Exception { + return StsClient.Builder.newInstance() + .id(resultSet.getString(statements.getIdColumn())) + .did(resultSet.getString(statements.getDidColumn())) + .name(resultSet.getString(statements.getNameColumn())) + .clientId(resultSet.getString(statements.getClientIdColumn())) + .secretAlias(resultSet.getString(statements.getSecretAliasColumn())) + .privateKeyAlias(resultSet.getString(statements.getPrivateKeyAliasColumn())) + .publicKeyReference(resultSet.getString(statements.getPublicKeyReferenceColumn())) + .createdAt(resultSet.getLong(statements.getCreatedAtColumn())) + .build(); + } +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtension.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtension.java new file mode 100644 index 00000000000..a805a2614d4 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtension.java @@ -0,0 +1,76 @@ +/* + * 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.iam.identitytrust.sts.store; + + +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.StsClientStatements; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapper; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import static org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry.DEFAULT_DATASOURCE; + +@Provides({ StsClientStore.class }) +@Extension(value = "SQL sts client store") +public class SqlStsClientStoreExtension implements ServiceExtension { + + @Setting(value = "The datasource to be used", defaultValue = DEFAULT_DATASOURCE) + public static final String DATASOURCE_NAME = "edc.sql.store.stsclient.datasource"; + + @Inject + private DataSourceRegistry dataSourceRegistry; + + @Inject + private TransactionContext transactionContext; + + @Inject(required = false) + private StsClientStatements statements; + + @Inject + private QueryExecutor queryExecutor; + + @Inject + private TypeManager typeManager; + + @Inject + private SqlSchemaBootstrapper sqlSchemaBootstrapper; + + @Override + public void initialize(ServiceExtensionContext context) { + var dataSourceName = context.getSetting(DATASOURCE_NAME, DEFAULT_DATASOURCE); + + var sqlStore = new SqlStsClientStore(dataSourceRegistry, dataSourceName, transactionContext, typeManager.getMapper(), + getStatementImpl(), queryExecutor); + + context.registerService(StsClientStore.class, sqlStore); + + sqlSchemaBootstrapper.addStatementFromResource(dataSourceName, "sts-client-schema.sql"); + } + + private StsClientStatements getStatementImpl() { + return statements == null ? new PostgresDialectStatements() : statements; + } + +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/BaseSqlDialectStatements.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/BaseSqlDialectStatements.java new file mode 100644 index 00000000000..ac9c79cbe43 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/BaseSqlDialectStatements.java @@ -0,0 +1,83 @@ +/* + * 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.iam.identitytrust.sts.store.schema; + +import org.eclipse.edc.iam.identitytrust.sts.store.schema.postgres.StsClientMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.SqlOperatorTranslator; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +public class BaseSqlDialectStatements implements StsClientStatements { + + protected final SqlOperatorTranslator operatorTranslator; + + public BaseSqlDialectStatements(SqlOperatorTranslator operatorTranslator) { + this.operatorTranslator = operatorTranslator; + } + + @Override + public String getDeleteByIdTemplate() { + return executeStatement().delete(getStsClientTable(), getIdColumn()); + } + + @Override + public String getFindByTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getStsClientTable(), getIdColumn()); + } + + @Override + public String getFindByClientIdTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getStsClientTable(), getClientIdColumn()); + } + + @Override + public String getInsertTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getNameColumn()) + .column(getClientIdColumn()) + .column(getDidColumn()) + .column(getSecretAliasColumn()) + .column(getPrivateKeyAliasColumn()) + .column(getPublicKeyReferenceColumn()) + .column(getCreatedAtColumn()) + .insertInto(getStsClientTable()); + } + + + @Override + public String getUpdateTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getNameColumn()) + .column(getClientIdColumn()) + .column(getDidColumn()) + .column(getSecretAliasColumn()) + .column(getPrivateKeyAliasColumn()) + .column(getPublicKeyReferenceColumn()) + .column(getCreatedAtColumn()) + .update(getStsClientTable(), getIdColumn()); + + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + var select = format("SELECT * FROM %s", getStsClientTable()); + return new SqlQueryStatement(select, querySpec, new StsClientMapping(this), operatorTranslator); + } + +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/StsClientStatements.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/StsClientStatements.java new file mode 100644 index 00000000000..5a71e407ae3 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/StsClientStatements.java @@ -0,0 +1,75 @@ +/* + * 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.iam.identitytrust.sts.store.schema; + +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.statement.SqlStatements; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +/** + * Defines all statements that are needed for the {@link StsClient} store + */ +public interface StsClientStatements extends SqlStatements { + + default String getIdColumn() { + return "id"; + } + + default String getDidColumn() { + return "did"; + } + + default String getClientIdColumn() { + return "client_id"; + } + + default String getStsClientTable() { + return "edc_sts_client"; + } + + default String getNameColumn() { + return "name"; + } + + default String getSecretAliasColumn() { + return "secret_alias"; + } + + default String getPrivateKeyAliasColumn() { + return "private_key_alias"; + } + + default String getPublicKeyReferenceColumn() { + return "public_key_reference"; + } + + default String getCreatedAtColumn() { + return "created_at"; + } + + String getDeleteByIdTemplate(); + + String getFindByTemplate(); + + String getFindByClientIdTemplate(); + + String getInsertTemplate(); + + String getUpdateTemplate(); + + SqlQueryStatement createQuery(QuerySpec querySpec); + +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/PostgresDialectStatements.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 00000000000..1d0941a86bb --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,35 @@ +/* + * 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.iam.identitytrust.sts.store.schema.postgres; + +import org.eclipse.edc.iam.identitytrust.sts.store.schema.BaseSqlDialectStatements; +import org.eclipse.edc.sql.dialect.PostgresDialect; +import org.eclipse.edc.sql.translation.PostgresqlOperatorTranslator; + +/** + * Contains Postgres-specific SQL statements + */ +public class PostgresDialectStatements extends BaseSqlDialectStatements { + + public PostgresDialectStatements() { + super(new PostgresqlOperatorTranslator()); + } + + @Override + public String getFormatAsJsonOperator() { + return PostgresDialect.getJsonCastOperator(); + } + +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/StsClientMapping.java b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/StsClientMapping.java new file mode 100644 index 00000000000..3f9a4397b7a --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/java/org/eclipse/edc/iam/identitytrust/sts/store/schema/postgres/StsClientMapping.java @@ -0,0 +1,36 @@ +/* + * 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.iam.identitytrust.sts.store.schema.postgres; + +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.StsClientStatements; +import org.eclipse.edc.sql.translation.TranslationMapping; + +/** + * Maps fields of a {@link StsClient} onto the + * corresponding SQL schema (= column names) + */ +public class StsClientMapping extends TranslationMapping { + public StsClientMapping(StsClientStatements statements) { + add("id", statements.getIdColumn()); + add("name", statements.getNameColumn()); + add("clientId", statements.getClientIdColumn()); + add("did", statements.getDidColumn()); + add("secretAlias", statements.getSecretAliasColumn()); + add("privateKeyAlias", statements.getPrivateKeyAliasColumn()); + add("publicKeyReference", statements.getPublicKeyReferenceColumn()); + add("createdAt", statements.getCreatedAtColumn()); + } +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/common/store/sql/sts-client-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000000..eb6e0d3b200 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# 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.iam.identitytrust.sts.store.SqlStsClientStoreExtension \ No newline at end of file diff --git a/extensions/common/store/sql/sts-client-store-sql/src/main/resources/sts-client-schema.sql b/extensions/common/store/sql/sts-client-store-sql/src/main/resources/sts-client-schema.sql new file mode 100644 index 00000000000..f2b6db43c44 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/main/resources/sts-client-schema.sql @@ -0,0 +1,18 @@ +-- THIS SCHEMA HAS BEEN WRITTEN AND TESTED ONLY FOR POSTGRES + +-- table: edc_sts_client + +CREATE TABLE IF NOT EXISTS edc_sts_client +( + id VARCHAR NOT NULL PRIMARY KEY, + client_id VARCHAR NOT NULL, + did VARCHAR NOT NULL, + name VARCHAR NOT NULL, + secret_alias VARCHAR NOT NULL, + private_key_alias VARCHAR NOT NULL, + public_key_reference VARCHAR NOT NULL, + created_at BIGINT NOT NULL +); + + +CREATE UNIQUE INDEX IF NOT EXISTS sts_client_client_id_index ON edc_sts_client (client_id); \ No newline at end of file diff --git a/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtensionTest.java b/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtensionTest.java new file mode 100644 index 00000000000..a3ca991da53 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreExtensionTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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.iam.identitytrust.sts.store; + +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.json.JacksonTypeManager; +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.types.TypeManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.iam.identitytrust.sts.store.SqlStsClientStoreExtension.DATASOURCE_NAME; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class SqlStsClientStoreExtensionTest { + + @BeforeEach + void setUp(ServiceExtensionContext context) { + context.registerService(TypeManager.class, new JacksonTypeManager()); + } + + @Test + void shouldInitializeTheStore(SqlStsClientStoreExtension extension, ServiceExtensionContext context) { + var config = mock(Config.class); + when(context.getConfig()).thenReturn(config); + when(config.getString(any(), any())).thenReturn("test"); + + extension.initialize(context); + + var service = context.getService(StsClientStore.class); + assertThat(service).isInstanceOf(SqlStsClientStore.class); + + verify(config).getString(eq(DATASOURCE_NAME), any()); + } +} diff --git a/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreTest.java b/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreTest.java new file mode 100644 index 00000000000..dab16bb1f77 --- /dev/null +++ b/extensions/common/store/sql/sts-client-store-sql/src/test/java/org/eclipse/edc/iam/identitytrust/sts/store/SqlStsClientStoreTest.java @@ -0,0 +1,61 @@ +/* + * 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.iam.identitytrust.sts.store; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.iam.identitytrust.sts.spi.store.fixtures.StsClientStoreTestBase; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.BaseSqlDialectStatements; +import org.eclipse.edc.iam.identitytrust.sts.store.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.json.JacksonTypeManager; +import org.eclipse.edc.junit.annotations.ComponentTest; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.policy.model.PolicyRegistrationTypes; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.testfixtures.PostgresqlStoreSetupExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +@ComponentTest +@ExtendWith(PostgresqlStoreSetupExtension.class) +public class SqlStsClientStoreTest extends StsClientStoreTestBase { + + private final BaseSqlDialectStatements sqlStatements = new PostgresDialectStatements(); + + private SqlStsClientStore stsClientStore; + + @BeforeEach + void setUp(PostgresqlStoreSetupExtension setupExtension, QueryExecutor queryExecutor) { + var typeManager = new JacksonTypeManager(); + typeManager.registerTypes(PolicyRegistrationTypes.TYPES.toArray(Class[]::new)); + + stsClientStore = new SqlStsClientStore(setupExtension.getDataSourceRegistry(), setupExtension.getDatasourceName(), + setupExtension.getTransactionContext(), new ObjectMapper(), sqlStatements, queryExecutor); + + var schema = TestUtils.getResourceFileContentAsString("sts-client-schema.sql"); + setupExtension.runQuery(schema); + } + + @AfterEach + void tearDown(PostgresqlStoreSetupExtension setupExtension) { + setupExtension.runQuery("DROP TABLE " + sqlStatements.getStsClientTable() + " CASCADE"); + } + + @Override + protected StsClientStore getStsClientStore() { + return stsClientStore; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e1bee6771db..ef9c3cc4c01 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -155,6 +155,7 @@ include(":extensions:common:validator:validator-data-address-http-data") include(":extensions:common:validator:validator-data-address-kafka") include(":extensions:common:vault:vault-hashicorp") include(":extensions:common:store:sql:edr-index-sql") +include(":extensions:common:store:sql:sts-client-store-sql") include(":extensions:common:api:control-api-configuration") include(":extensions:common:api:management-api-configuration") diff --git a/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/model/StsClient.java b/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/model/StsClient.java index d43da7cef2f..e447256d535 100644 --- a/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/model/StsClient.java +++ b/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/model/StsClient.java @@ -14,6 +14,7 @@ package org.eclipse.edc.iam.identitytrust.sts.spi.model; +import org.eclipse.edc.spi.entity.Entity; import org.eclipse.edc.spi.security.Vault; import java.util.Objects; @@ -21,8 +22,7 @@ /** * The {@link StsClient} contains information about STS clients. */ -public class StsClient { - private String id; +public class StsClient extends Entity { private String clientId; private String did; private String name; @@ -33,15 +33,6 @@ public class StsClient { private StsClient() { } - /** - * Unique identifier of the {@link StsClient} - * - * @return The ID of the Client - */ - public String getId() { - return id; - } - /** * The alias of the {@link StsClient} secret stored in the {@link Vault} * @@ -101,63 +92,69 @@ public String getPublicKeyReference() { } - public static class Builder { + public static class Builder extends Entity.Builder { - private final StsClient client; - private Builder(StsClient client) { - this.client = client; + private Builder() { + super(new StsClient()); } public static Builder newInstance() { - return new Builder(new StsClient()); + return new Builder(); } public Builder id(String id) { - client.id = id; + entity.id = id; + return this; + } + + @Override + public Builder self() { return this; } + @Override + public StsClient build() { + Objects.requireNonNull(entity.id, "Client id"); + Objects.requireNonNull(entity.clientId, "Client client_id"); + Objects.requireNonNull(entity.name, "Client name"); + Objects.requireNonNull(entity.did, "Client DID"); + Objects.requireNonNull(entity.secretAlias, "Client secret alias"); + Objects.requireNonNull(entity.privateKeyAlias, "Client private key alias"); + Objects.requireNonNull(entity.publicKeyReference, "Client public key reference"); + return super.build(); + } + public Builder clientId(String clientId) { - client.clientId = clientId; + entity.clientId = clientId; return this; } public Builder name(String name) { - client.name = name; + entity.name = name; return this; } public Builder did(String did) { - client.did = did; + entity.did = did; return this; } public Builder secretAlias(String secretAlias) { - client.secretAlias = secretAlias; + entity.secretAlias = secretAlias; return this; } public Builder privateKeyAlias(String privateKeyAlias) { - client.privateKeyAlias = privateKeyAlias; + entity.privateKeyAlias = privateKeyAlias; return this; } public Builder publicKeyReference(String publicKeyReference) { - client.publicKeyReference = publicKeyReference; + entity.publicKeyReference = publicKeyReference; return this; } - public StsClient build() { - Objects.requireNonNull(client.id, "Client id"); - Objects.requireNonNull(client.clientId, "Client client_id"); - Objects.requireNonNull(client.name, "Client name"); - Objects.requireNonNull(client.did, "Client DID"); - Objects.requireNonNull(client.secretAlias, "Client secret alias"); - Objects.requireNonNull(client.privateKeyAlias, "Client private key alias"); - return client; - } - } } diff --git a/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/StsClientStore.java b/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/StsClientStore.java index 203221c9913..27e609ef375 100644 --- a/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/StsClientStore.java +++ b/spi/common/identity-trust-sts-spi/src/main/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/StsClientStore.java @@ -17,7 +17,11 @@ import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; +import org.jetbrains.annotations.NotNull; + +import java.util.stream.Stream; /** * Persists and retrieves {@link StsClient}s. @@ -25,13 +29,44 @@ @ExtensionPoint public interface StsClientStore { + String CLIENT_EXISTS_TEMPLATE = "Client with ID %s already exists"; + String CLIENT_NOT_FOUND_BY_CLIENT_ID_TEMPLATE = "Client with clientID %s not found"; + String CLIENT_NOT_FOUND_BY_ID_TEMPLATE = "Client with id %s not found"; + /** * Stores the {@link StsClient} * - * @param client The client + * @param stsClient The client * @return successful when the client is stored, failure otherwise */ - StoreResult create(StsClient client); + StoreResult create(StsClient stsClient); + + + /** + * Update the {@link StsClient} if am sts client with the same ID exists. + * + * @param stsClient {@link StsClient} to update. + * @return {@link StoreResult#success()} if the sts client was updates, {@link StoreResult#notFound(String)} if the sts client identified by the ID was not found. + */ + StoreResult update(StsClient stsClient); + + /** + * Returns all the sts clients in the store that are covered by a given {@link QuerySpec}. + *

+ * Note: supplying a sort field that does not exist on the {@link StsClient} may cause some implementations + * to return an empty Stream, others will return an unsorted Stream, depending on the backing storage + * implementation. + */ + @NotNull + Stream findAll(QuerySpec spec); + + /** + * Returns an {@link StsClient} by its id + * + * @param id id of the client + * @return the client successful if found, failure otherwise + */ + StoreResult findById(String id); /** * Returns an {@link StsClient} by its clientId @@ -41,4 +76,11 @@ public interface StsClientStore { */ StoreResult findByClientId(String clientId); + /** + * Deletes the sts client with the given id. + * + * @param id A String that represents the {@link StsClient} ID, in most cases this will be a UUID. + * @return {@link StoreResult#success()}} if the sts client was deleted, {@link StoreResult#notFound(String)} if the sts client was not found in the store. + */ + StoreResult deleteById(String id); } diff --git a/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/StsClientStoreTestBase.java b/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/StsClientStoreTestBase.java index 89094154209..605845d2adc 100644 --- a/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/StsClientStoreTestBase.java +++ b/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/StsClientStoreTestBase.java @@ -14,16 +14,38 @@ package org.eclipse.edc.iam.identitytrust.sts.spi.store.fixtures; +import org.eclipse.edc.iam.identitytrust.sts.spi.model.StsClient; import org.eclipse.edc.iam.identitytrust.sts.spi.store.StsClientStore; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.query.SortOrder; import org.eclipse.edc.spi.result.StoreFailure; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.Comparator; +import java.util.List; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.iam.identitytrust.sts.spi.store.fixtures.TestFunctions.createClient; +import static org.eclipse.edc.iam.identitytrust.sts.spi.store.fixtures.TestFunctions.createClientBuilder; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.spi.query.Criterion.criterion; +import static org.eclipse.edc.spi.result.StoreFailure.Reason.ALREADY_EXISTS; +import static org.eclipse.edc.spi.result.StoreFailure.Reason.NOT_FOUND; /** * Base compliance tests for implementors of {@link StsClientStore}. @@ -36,35 +58,275 @@ protected String getRandomId() { return UUID.randomUUID().toString(); } + private List createClients(int size) { + return IntStream.range(0, size).mapToObj(i -> createClient("id" + i)) + .toList(); + } + + private void delay(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void saveClients(List clients) { + clients.forEach(getStsClientStore()::create); + } + @Nested class Create { @Test @DisplayName("Save a single client that not exists") void create() { - var client = TestFunctions.createClient(getRandomId()); + var client = createClient(getRandomId()); assertThat(getStsClientStore().create(client)).isSucceeded(); var clientFromDb = getStsClientStore().findByClientId(client.getId()).getContent(); assertThat(client).usingRecursiveComparison().isEqualTo(clientFromDb); } + + + @Test + @DisplayName("Saves multiple client that not exists") + void create_MultipleClients() { + + var clients = IntStream.range(0, 10) + .mapToObj(i -> createClient("id" + i)) + .peek(getStsClientStore()::create) + .toList(); + + var result = getStsClientStore().findAll(QuerySpec.max()); + + assertThat(result).hasSize(clients.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsAll(clients); + } + + @Test + @DisplayName("Shouldn't save a single client that already exists") + void alreadyExist_shouldNotUpdate() { + var client = createClient("id"); + getStsClientStore().create(client); + var saveResult = getStsClientStore().create(createClient("id")); + + assertThat(saveResult.failed()).isTrue(); + assertThat(saveResult.reason()).isEqualTo(ALREADY_EXISTS); + + var result = getStsClientStore().findAll(QuerySpec.max()); + + assertThat(result).hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(client); + } + } + + @Nested + class FindAll { + + @ParameterizedTest + @ValueSource(ints = { 49, 50, 51, 100 }) + void verifyQueryDefaults(int size) { + var all = IntStream.range(0, size).mapToObj(i -> createClient("id" + i)) + .peek(getStsClientStore()::create) + .collect(Collectors.toList()); + + assertThat(getStsClientStore().findAll(QuerySpec.max())).hasSize(size) + .usingRecursiveFieldByFieldElementComparator() + .isSubsetOf(all); + } + + @Test + @DisplayName("Find all clients with limit and offset") + void withSpec() { + var limit = 20; + + IntStream.range(0, 50).mapToObj(i -> createClient("id" + i)) + .forEach(getStsClientStore()::create); + + var spec = QuerySpec.Builder.newInstance() + .limit(limit) + .offset(20) + .build(); + + var resultClients = getStsClientStore().findAll(spec); + + assertThat(resultClients).isNotNull().hasSize(limit); + } + + @ParameterizedTest + @ArgumentsSource(FilterArgumentProvider.class) + void query_withQuerySpec(String field, Function mapping) { + var clients = createClients(10); + saveClients(clients); + + + var client = createClientBuilder("id") + .name("client_name") + .clientId("client_id") + .did("did:web:client") + .secretAlias("secret_alias") + .privateKeyAlias("private_key_alias") + .publicKeyReference("public_key_reference") + .build(); + + getStsClientStore().create(client); + + var filter = Criterion.Builder.newInstance() + .operandLeft(field) + .operator("=") + .operandRight(mapping.apply(client)) + .build(); + + var results = getStsClientStore().findAll(QuerySpec.Builder.newInstance().filter(filter).build()); + + assertThat(results).usingRecursiveFieldByFieldElementComparator() + .containsOnly(client); + + } + + @Test + @DisplayName("Verify empty result when query contains a nonexistent value") + void queryByNonexistentValue() { + + var clients = createClients(20); + saveClients(clients); + + var spec = QuerySpec.Builder.newInstance() + .filter(List.of(new Criterion("client_id", "=", "somevalue"))) + .build(); + + assertThat(getStsClientStore().findAll(spec)).isEmpty(); + } + + @Test + void invalidOperator() { + + var stsClients = createClients(20); + saveClients(stsClients); + + var spec = QuerySpec.Builder.newInstance() + .filter(List.of(new Criterion("did", "sqrt", "foobar"))) //sqrt is invalid + .build(); + + assertThatThrownBy(() -> getStsClientStore().findAll(spec)).isInstanceOf(IllegalArgumentException.class); + } + + + @Test + void verifyPaging() { + var stsClients = createClients(10); + saveClients(stsClients); + + // page size fits + assertThat(getStsClientStore().findAll(QuerySpec.Builder.newInstance().offset(4).limit(2).build())).hasSize(2); + + // page size larger than collection + assertThat(getStsClientStore().findAll(QuerySpec.Builder.newInstance().offset(5).limit(100).build())).hasSize(5); + } + + @Test + void shouldReturnEmpty_whenQueryByInvalidKey() { + var stsClients = createClients(5); + saveClients(stsClients); + + var spec = QuerySpec.Builder.newInstance() + .filter(criterion("not-exist", "=", "some-value")) + .build(); + + assertThat(getStsClientStore().findAll(spec)).isEmpty(); + } + + @Test + void verifySorting() { + + var stsClients = IntStream.range(0, 10).mapToObj(idx -> { + delay(10); + return createClient("id" + idx); + }).toList(); + + saveClients(stsClients); + + + assertThat(getStsClientStore().findAll(QuerySpec.Builder.newInstance().sortField("createdAt").sortOrder(SortOrder.ASC).build())) + .hasSize(10) + .isSortedAccordingTo(Comparator.comparing(StsClient::getCreatedAt)); + + assertThat(getStsClientStore().findAll(QuerySpec.Builder.newInstance().sortField("createdAt").sortOrder(SortOrder.DESC).build())) + .hasSize(10) + .isSortedAccordingTo((c1, c2) -> Long.compare(c2.getCreatedAt(), c1.getCreatedAt())); + } + + @Test + void verifySorting_invalidProperty() { + var stsClients = createClients(10); + saveClients(stsClients); + + var query = QuerySpec.Builder.newInstance().sortField("not-exist").sortOrder(SortOrder.DESC).build(); + + // must actually collect, otherwise the stream is not materialized + assertThatThrownBy(() -> getStsClientStore().findAll(query).toList()).isInstanceOf(IllegalArgumentException.class); + } + + static class FilterArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + + return Stream.of( + Arguments.of("id", (Function) StsClient::getId), + Arguments.of("clientId", (Function) StsClient::getClientId), + Arguments.of("name", (Function) StsClient::getName), + Arguments.of("did", (Function) StsClient::getDid), + Arguments.of("secretAlias", (Function) StsClient::getSecretAlias), + Arguments.of("privateKeyAlias", (Function) StsClient::getPrivateKeyAlias), + Arguments.of("publicKeyReference", (Function) StsClient::getPublicKeyReference) + ); + } + } } @Nested class FindById { + @Test @DisplayName("Find client by ID that exists") void whenPresent() { - var client = TestFunctions.createClient(getRandomId()); + var client = createClient(getRandomId(), "alias", getRandomId()); getStsClientStore().create(client); - var policyFromDb = getStsClientStore().findByClientId(client.getId()).getContent(); + var clientFromDb = getStsClientStore().findById(client.getId()).getContent(); - assertThat(client).usingRecursiveComparison().isEqualTo(policyFromDb); + assertThat(client).usingRecursiveComparison().isEqualTo(clientFromDb); } @Test @DisplayName("Find client by ID when not exists") + void whenNonexistent() { + assertThat(getStsClientStore().findById("nonexistent")) + .isFailed() + .extracting(StoreFailure::getReason) + .isEqualTo(StoreFailure.Reason.NOT_FOUND); + } + } + + @Nested + class FindByClientId { + + @Test + @DisplayName("Find client by Client ID that exists") + void whenPresent() { + var client = createClient(getRandomId(), "alias", getRandomId()); + getStsClientStore().create(client); + + var clientFromDb = getStsClientStore().findByClientId(client.getClientId()).getContent(); + + assertThat(client).usingRecursiveComparison().isEqualTo(clientFromDb); + } + + @Test + @DisplayName("Find client by Client ID when not exists") void whenNonexistent() { assertThat(getStsClientStore().findByClientId("nonexistent")) .isFailed() @@ -72,4 +334,75 @@ void whenNonexistent() { .isEqualTo(StoreFailure.Reason.NOT_FOUND); } } + + @Nested + class Update { + @Test + @DisplayName("Update a non-existing Client") + void doesNotExist_shouldNotCreate() { + var client = createClient(getRandomId(), "alias", getRandomId()); + + var result = getStsClientStore().update(client); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason()).isEqualTo(NOT_FOUND); + + var existing = getStsClientStore().findAll(QuerySpec.max()); + + assertThat(existing).hasSize(0); + } + + @Test + @DisplayName("Update an existing client") + void exists() { + var client1 = createClient("id", "alias", getRandomId()); + var client2 = createClientBuilder("id") + .clientId(client1.getClientId()) + .name("nameChanged") + .privateKeyAlias("privateAliasChanged") + .publicKeyReference("publicRefChanged") + .did("didChanged") + .secretAlias("aliasChanged") + .build(); + + + getStsClientStore().create(client1); + getStsClientStore().update(client2); + + var clients = getStsClientStore().findAll(QuerySpec.none()).collect(Collectors.toList()); + + assertThat(clients).isNotNull().hasSize(1).first().satisfies(client -> { + assertThat(client.getId()).isEqualTo(client2.getId()); + assertThat(client.getName()).isEqualTo(client2.getName()); + assertThat(client.getDid()).isEqualTo(client2.getDid()); + assertThat(client.getSecretAlias()).isEqualTo(client2.getSecretAlias()); + assertThat(client.getPrivateKeyAlias()).isEqualTo(client2.getPrivateKeyAlias()); + assertThat(client.getPublicKeyReference()).isEqualTo(client2.getPublicKeyReference()); + assertThat(client.getClientId()).isEqualTo(client2.getClientId()); + }); + } + } + + @Nested + class Delete { + @Test + void shouldDelete() { + var client = createClient(getRandomId(), "alias", getRandomId()); + getStsClientStore().create(client); + assertThat(getStsClientStore().findAll(QuerySpec.max())).hasSize(1); + + var deleted = getStsClientStore().deleteById(client.getId()); + + assertThat(deleted.succeeded()).isTrue(); + assertThat(deleted.getContent()).isNotNull().usingRecursiveComparison().isEqualTo(client); + assertThat(getStsClientStore().findAll(QuerySpec.max())).isEmpty(); + } + + @Test + void shouldNotDelete_whenEntityDoesNotExist() { + var deleted = getStsClientStore().deleteById("test-id1"); + + assertThat(deleted).isFailed().extracting(StoreFailure::getReason).isEqualTo(NOT_FOUND); + } + } } diff --git a/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/TestFunctions.java b/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/TestFunctions.java index 1f041e35f9c..1ead959e0ba 100644 --- a/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/TestFunctions.java +++ b/spi/common/identity-trust-sts-spi/src/testFixtures/java/org/eclipse/edc/iam/identitytrust/sts/spi/store/fixtures/TestFunctions.java @@ -25,6 +25,10 @@ public static StsClient createClient(String id, String secretAlias) { return createClient(id, secretAlias, id, secretAlias, "did:example:subject"); } + public static StsClient createClient(String id, String secretAlias, String clientId) { + return createClient(id, secretAlias, clientId, secretAlias, "did:example:subject"); + } + public static StsClient createClient(String id, String secretAlias, String clientId, String publicKeyReference, String did) { return createClientBuilder(id) .clientId(clientId) diff --git a/system-tests/sts-api/sts-api-test-runner/build.gradle.kts b/system-tests/sts-api/sts-api-test-runner/build.gradle.kts index 867159ac0e6..5ed1534baad 100644 --- a/system-tests/sts-api/sts-api-test-runner/build.gradle.kts +++ b/system-tests/sts-api/sts-api-test-runner/build.gradle.kts @@ -29,6 +29,8 @@ dependencies { testCompileOnly(project(":system-tests:sts-api:sts-api-test-runtime")) testImplementation(testFixtures(project(":spi:common:identity-trust-sts-spi"))) + testImplementation(testFixtures(project(":extensions:common:sql:sql-core"))) + testImplementation(project(":extensions:common:transaction:transaction-local")) testImplementation(libs.nimbus.jwt) testImplementation(libs.bouncyCastle.bcpkixJdk18on) diff --git a/system-tests/sts-api/sts-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/sts/api/StsApiEndToEndTest.java b/system-tests/sts-api/sts-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/sts/api/StsApiEndToEndTest.java index 465ef2f15be..ab81fc7f04b 100644 --- a/system-tests/sts-api/sts-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/sts/api/StsApiEndToEndTest.java +++ b/system-tests/sts-api/sts-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/sts/api/StsApiEndToEndTest.java @@ -17,9 +17,13 @@ import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; import org.eclipse.edc.junit.extensions.EmbeddedRuntime; import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; +import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndInstance; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; import java.io.IOException; @@ -39,174 +43,218 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.iam.identitytrust.spi.SelfIssuedTokenConstants.PRESENTATION_TOKEN_CLAIM; import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.CLIENT_ID; +import static org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndInstance.createDatabase; import static org.eclipse.edc.util.io.Ports.getFreePort; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -@EndToEndTest -public class StsApiEndToEndTest extends StsEndToEndTestBase { +public class StsApiEndToEndTest { public static final int PORT = getFreePort(); public static final String BASE_STS = "http://localhost:" + PORT + "/sts"; private static final String GRANT_TYPE = "client_credentials"; - - @RegisterExtension - static RuntimePerClassExtension sts = new RuntimePerClassExtension(new EmbeddedRuntime( - "sts", - new HashMap<>() { - { - put("web.http.path", "/"); - put("web.http.port", String.valueOf(getFreePort())); - put("web.http.sts.path", "/sts"); - put("web.http.sts.port", String.valueOf(PORT)); - } - }, - ":system-tests:sts-api:sts-api-test-runtime" - )); - - @Test - void requestToken() throws ParseException { - var audience = "audience"; - var clientSecret = "client_secret"; - var expiresIn = 300; - - var client = initClient(clientSecret); - - var params = Map.of( - "client_id", client.getClientId(), - "audience", audience, - "client_secret", clientSecret); - - var token = tokenRequest(params) - .statusCode(200) - .contentType(JSON) - .body("access_token", notNullValue()) - .body("expires_in", is(expiresIn)) - .extract() - .body() - .jsonPath().getString("access_token"); - - assertThat(parseClaims(token)) - .containsEntry(ISSUER, client.getDid()) - .containsEntry(SUBJECT, client.getDid()) - .containsEntry(AUDIENCE, List.of(audience)) - .doesNotContainKey(CLIENT_ID) - .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); - } - - - @Test - void requestToken_withBearerScope() throws ParseException { - var clientSecret = "client_secret"; - var audience = "audience"; - var bearerAccessScope = "org.test.Member:read org.test.GoldMember:read"; - var expiresIn = 300; - - var client = initClient(clientSecret); - - - var params = Map.of( - "client_id", client.getClientId(), - "audience", audience, - "bearer_access_scope", bearerAccessScope, - "client_secret", clientSecret); - - var token = tokenRequest(params) - .statusCode(200) - .contentType(JSON) - .body("access_token", notNullValue()) - .body("expires_in", is(expiresIn)) - .extract() - .body() - .jsonPath().getString("access_token"); - - - assertThat(parseClaims(token)) - .containsEntry(ISSUER, client.getDid()) - .containsEntry(SUBJECT, client.getDid()) - .containsEntry(AUDIENCE, List.of(audience)) - .doesNotContainKey(CLIENT_ID) - .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT) - .hasEntrySatisfying(PRESENTATION_TOKEN_CLAIM, (accessToken) -> { - assertThat(parseClaims((String) accessToken)) - .containsEntry(ISSUER, client.getDid()) - .containsEntry(SUBJECT, audience) - .containsEntry(AUDIENCE, List.of(client.getDid())) - .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); - }); - } - - @Test - void requestToken_withAttachedAccessScope() throws IOException, ParseException { - var clientSecret = "client_secret"; - var audience = "audience"; - var token = "test_token"; - var expiresIn = 300; - var client = initClient(clientSecret); - - - var params = Map.of( - "client_id", client.getClientId(), - "audience", audience, - "token", token, - "client_secret", clientSecret); - - var accessToken = tokenRequest(params) - .statusCode(200) - .contentType(JSON) - .body("access_token", notNullValue()) - .body("expires_in", is(expiresIn)) - .extract() - .body() - .jsonPath().getString("access_token"); - - - assertThat(parseClaims(accessToken)) - .containsEntry(ISSUER, client.getDid()) - .containsEntry(SUBJECT, client.getDid()) - .containsEntry(AUDIENCE, List.of(audience)) - .doesNotContainKey(CLIENT_ID) - .containsEntry(PRESENTATION_TOKEN_CLAIM, token) - .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); - } - - @Test - void requestToken_shouldReturnError_whenClientNotFound() { - - var clientId = "client_id"; - var clientSecret = "client_secret"; - var audience = "audience"; - - var params = Map.of( - "client_id", clientId, - "audience", audience, - "client_secret", clientSecret); - - tokenRequest(params) - .statusCode(401) - .contentType(JSON); - } - - protected ValidatableResponse tokenRequest(Map params) { - - var req = baseRequest() - .contentType("application/x-www-form-urlencoded") - .formParam("grant_type", GRANT_TYPE); - params.forEach(req::formParam); - return req.post("/token").then(); + abstract static class Tests extends StsEndToEndTestBase { + + @Test + void requestToken() throws ParseException { + var audience = "audience"; + var clientSecret = "client_secret"; + var expiresIn = 300; + + var client = initClient(clientSecret); + + var params = Map.of( + "client_id", client.getClientId(), + "audience", audience, + "client_secret", clientSecret); + + var token = tokenRequest(params) + .statusCode(200) + .contentType(JSON) + .body("access_token", notNullValue()) + .body("expires_in", is(expiresIn)) + .extract() + .body() + .jsonPath().getString("access_token"); + + assertThat(parseClaims(token)) + .containsEntry(ISSUER, client.getDid()) + .containsEntry(SUBJECT, client.getDid()) + .containsEntry(AUDIENCE, List.of(audience)) + .doesNotContainKey(CLIENT_ID) + .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); + } + + @Test + void requestToken_withBearerScope() throws ParseException { + var clientSecret = "client_secret"; + var audience = "audience"; + var bearerAccessScope = "org.test.Member:read org.test.GoldMember:read"; + var expiresIn = 300; + + var client = initClient(clientSecret); + + + var params = Map.of( + "client_id", client.getClientId(), + "audience", audience, + "bearer_access_scope", bearerAccessScope, + "client_secret", clientSecret); + + var token = tokenRequest(params) + .statusCode(200) + .contentType(JSON) + .body("access_token", notNullValue()) + .body("expires_in", is(expiresIn)) + .extract() + .body() + .jsonPath().getString("access_token"); + + + assertThat(parseClaims(token)) + .containsEntry(ISSUER, client.getDid()) + .containsEntry(SUBJECT, client.getDid()) + .containsEntry(AUDIENCE, List.of(audience)) + .doesNotContainKey(CLIENT_ID) + .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT) + .hasEntrySatisfying(PRESENTATION_TOKEN_CLAIM, (accessToken) -> { + assertThat(parseClaims((String) accessToken)) + .containsEntry(ISSUER, client.getDid()) + .containsEntry(SUBJECT, audience) + .containsEntry(AUDIENCE, List.of(client.getDid())) + .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); + }); + } + + @Test + void requestToken_withAttachedAccessScope() throws IOException, ParseException { + var clientSecret = "client_secret"; + var audience = "audience"; + var token = "test_token"; + var expiresIn = 300; + var client = initClient(clientSecret); + + + var params = Map.of( + "client_id", client.getClientId(), + "audience", audience, + "token", token, + "client_secret", clientSecret); + + var accessToken = tokenRequest(params) + .statusCode(200) + .contentType(JSON) + .body("access_token", notNullValue()) + .body("expires_in", is(expiresIn)) + .extract() + .body() + .jsonPath().getString("access_token"); + + + assertThat(parseClaims(accessToken)) + .containsEntry(ISSUER, client.getDid()) + .containsEntry(SUBJECT, client.getDid()) + .containsEntry(AUDIENCE, List.of(audience)) + .doesNotContainKey(CLIENT_ID) + .containsEntry(PRESENTATION_TOKEN_CLAIM, token) + .containsKeys(JWT_ID, EXPIRATION_TIME, ISSUED_AT); + } + + @Test + void requestToken_shouldReturnError_whenClientNotFound() { + + var clientId = "client_id"; + var clientSecret = "client_secret"; + var audience = "audience"; + + var params = Map.of( + "client_id", clientId, + "audience", audience, + "client_secret", clientSecret); + + tokenRequest(params) + .statusCode(401) + .contentType(JSON); + } + + protected ValidatableResponse tokenRequest(Map params) { + + var req = baseRequest() + .contentType("application/x-www-form-urlencoded") + .formParam("grant_type", GRANT_TYPE); + params.forEach(req::formParam); + return req.post("/token").then(); + } + + protected RequestSpecification baseRequest() { + return given() + .port(PORT) + .baseUri(BASE_STS) + .when(); + } } - protected RequestSpecification baseRequest() { - return given() - .port(PORT) - .baseUri(BASE_STS) - .when(); + @Nested + @EndToEndTest + class InMemory extends Tests { + + @RegisterExtension + static RuntimePerClassExtension sts = new RuntimePerClassExtension(new EmbeddedRuntime( + "sts", + new HashMap<>() { + { + put("web.http.path", "/"); + put("web.http.port", String.valueOf(getFreePort())); + put("web.http.sts.path", "/sts"); + put("web.http.sts.port", String.valueOf(PORT)); + } + }, + ":system-tests:sts-api:sts-api-test-runtime" + )); + + @Override + protected RuntimePerClassExtension getRuntime() { + return sts; + } } - @Override - protected RuntimePerClassExtension getRuntime() { - return sts; + @Nested + @PostgresqlIntegrationTest + class Postgres extends Tests { + + + @RegisterExtension + static RuntimePerClassExtension sts = new RuntimePerClassExtension(new EmbeddedRuntime( + "sts", + new HashMap<>() { + { + put("web.http.path", "/"); + put("web.http.port", String.valueOf(getFreePort())); + put("web.http.sts.path", "/sts"); + put("web.http.sts.port", String.valueOf(PORT)); + put("edc.datasource.default.url", PostgresqlEndToEndInstance.JDBC_URL_PREFIX + "runtime"); + put("edc.datasource.default.user", PostgresqlEndToEndInstance.USER); + put("edc.datasource.default.password", PostgresqlEndToEndInstance.PASSWORD); + put("edc.sql.schema.autocreate", "true"); + } + }, + ":system-tests:sts-api:sts-api-test-runtime", + ":extensions:common:store:sql:sts-client-store-sql", + ":extensions:common:sql:sql-pool:sql-pool-apache-commons", + ":extensions:common:transaction:transaction-local" + )) { + @Override + public void beforeAll(ExtensionContext extensionContext) { + createDatabase("runtime"); + super.beforeAll(extensionContext); + } + }; + + @Override + protected RuntimePerClassExtension getRuntime() { + return sts; + } } }