Skip to content

Commit

Permalink
feat: STS client SQL store implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood committed Sep 12, 2024
1 parent d5b50ad commit e207d3a
Show file tree
Hide file tree
Showing 24 changed files with 855 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@
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)
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;
}

@Provider(isDefault = true)
public StsClientStore clientStore() {
return new InMemoryStsClientStore();
return new InMemoryStsClientStore(criterionOperatorRegistry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,19 +35,54 @@
*/
public class InMemoryStsClientStore implements StsClientStore {

// we store it by clientId
private final Map<String, StsClient> clients = new ConcurrentHashMap<>();
private final QueryResolver<StsClient> queryResolver;


public InMemoryStsClientStore(CriterionOperatorRegistry criterionOperatorRegistry) {
queryResolver = new ReflectionBasedQueryResolver<>(StsClient.class, criterionOperatorRegistry);
}

@Override
public StoreResult<StsClient> create(StsClient client) {
return Optional.ofNullable(clients.putIfAbsent(client.getClientId(), client))
.map(old -> StoreResult.<StsClient>alreadyExists(format("Client with id %s already exists", client.getClientId())))
.map(old -> StoreResult.<StsClient>alreadyExists(format(CLIENT_EXISTS_TEMPLATE, client.getClientId())))
.orElseGet(() -> StoreResult.success(client));
}

@Override
public StoreResult<StsClient> findByClientId(String id) {
return Optional.ofNullable(clients.get(id))
public StoreResult<Void> update(StsClient stsClient) {
var prev = clients.replace(stsClient.getClientId(), stsClient);
return Optional.ofNullable(prev)
.map(a -> StoreResult.<Void>success())
.orElse(StoreResult.notFound(format(CLIENT_NOT_FOUND_BY_ID_TEMPLATE, stsClient.getId())));
}

@Override
public @NotNull Stream<StsClient> findAll(QuerySpec spec) {
return queryResolver.query(clients.values().stream(), spec);
}

@Override
public StoreResult<StsClient> 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<StsClient> 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<StsClient> deleteById(String id) {
return findById(id)
.onSuccess(client -> clients.remove(client.getClientId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,7 +25,7 @@ public class InMemoryStsClientStoreTest extends StsClientStoreTestBase {

@BeforeEach
void setUp() {
store = new InMemoryStsClientStore();
store = new InMemoryStsClientStore(CriterionOperatorRegistryImpl.ofDefaults());
}

@Override
Expand Down
31 changes: 31 additions & 0 deletions extensions/common/store/sql/sts-client-store-sql/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")))

}
Original file line number Diff line number Diff line change
@@ -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<StsClient> 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<Void> 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<StsClient> 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<StsClient> 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<StsClient> 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<StsClient> 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();
}
}
Loading

0 comments on commit e207d3a

Please sign in to comment.