Skip to content

Commit

Permalink
add participantcontext service impl
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jan 16, 2024
1 parent 4f4ee20 commit 2baa5f5
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 1 deletion.
11 changes: 11 additions & 0 deletions core/identity-hub-participants/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
`java-library`
}

dependencies {
api(project(":spi:identity-hub-spi"))
api(project(":spi:identity-hub-store-spi"))
api(libs.edc.spi.transaction)

testImplementation(libs.edc.junit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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
*
*/

package org.eclipse.edc.identityhub.participantcontext;

import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.RandomStringGenerator;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.transaction.spi.TransactionContext;

import static org.eclipse.edc.spi.result.ServiceResult.fromFailure;
import static org.eclipse.edc.spi.result.ServiceResult.notFound;
import static org.eclipse.edc.spi.result.ServiceResult.success;

/**
* Default implementation of the {@link ParticipantContextService}. Uses a {@link Vault} to store API tokens and a {@link RandomStringGenerator}
* to generate API tokens. Please use a generator that produces Strings of a reasonable length.
* <p>
* This service is transactional.
*/
public class ParticipantContextServiceImpl implements ParticipantContextService {

private final ParticipantContextStore participantContextStore;
private final Vault vault;
private final TransactionContext transactionContext;
private final RandomStringGenerator tokenGenerator;

public ParticipantContextServiceImpl(ParticipantContextStore participantContextStore, Vault vault, TransactionContext transactionContext, RandomStringGenerator tokenGenerator) {
this.participantContextStore = participantContextStore;
this.vault = vault;
this.transactionContext = transactionContext;
this.tokenGenerator = tokenGenerator;
}

@Override
public ServiceResult<Void> createParticipantContext(ParticipantContext context) {
return transactionContext.execute(() -> {
var storeRes = participantContextStore.create(context);
return storeRes.succeeded() ?
success() :
fromFailure(storeRes);
});
}

@Override
public ServiceResult<ParticipantContext> getParticipantContext(String participantId) {
return transactionContext.execute(() -> {
var res = participantContextStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("participantContext", "=", participantId)).build());
if (res.succeeded()) {
return res.getContent().findFirst()
.map(ServiceResult::success)
.orElse(notFound("No ParticipantContext with ID '%s' was found.".formatted(participantId)));
}
return fromFailure(res);
});
}

@Override
public ServiceResult<Void> deleteParticipantContext(String participantId) {
return transactionContext.execute(() -> {
var res = participantContextStore.deleteById(participantId);
return res.succeeded() ? ServiceResult.success() : ServiceResult.fromFailure(res);
});
}

@Override
public ServiceResult<String> regenerateApiToken(String participantId) {
return transactionContext.execute(() -> {
var participantContext = getParticipantContext(participantId);
if (participantContext.failed()) {
return participantContext.map(pc -> null);
}
var alias = participantContext.getContent().getApiTokenAlias();

var newToken = tokenGenerator.generate();
return vault.storeSecret(alias, newToken).map(unused -> ServiceResult.success(newToken)).orElse(f -> ServiceResult.conflict("Could not store new API token: %s.".formatted(f.getFailureDetail())));
});
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
# 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
#
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* 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
*
*/

package org.eclipse.edc.identityhub.participantcontext;

import org.assertj.core.api.Assertions;
import org.eclipse.edc.identityhub.spi.RandomStringGenerator;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState;
import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceFailure;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.transaction.spi.NoopTransactionContext;
import org.junit.jupiter.api.Test;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.stream.Stream;

import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

class ParticipantContextServiceImplTest {

private final Vault vault = mock();
private final ParticipantContextStore participantContextStore = mock();
private final SecureRandom secureRandom = new SecureRandom();
// generates 64 random bytes and base64-encodes them
private final RandomStringGenerator randomBase64Generator = () -> {
byte[] array = new byte[64];
secureRandom.nextBytes(array);
var enc = Base64.getEncoder();
return enc.encodeToString(array);
};
private final ParticipantContextServiceImpl participantContextService = new ParticipantContextServiceImpl(participantContextStore, vault, new NoopTransactionContext(), randomBase64Generator);

@Test
void createParticipantContext() {
when(participantContextStore.create(any())).thenReturn(StoreResult.success());

var ctx = createContext();
assertThat(participantContextService.createParticipantContext(ctx))
.isSucceeded();

verify(participantContextStore).create(any());
verifyNoMoreInteractions(vault, participantContextStore);
}

@Test
void createParticipantContext_storageFails() {
when(participantContextStore.create(any())).thenReturn(StoreResult.success());

var ctx = createContext();
assertThat(participantContextService.createParticipantContext(ctx))
.isSucceeded();

verify(participantContextStore).create(any());
verifyNoMoreInteractions(vault, participantContextStore);
}

@Test
void createParticipantContext_whenExists() {
when(participantContextStore.create(any())).thenReturn(StoreResult.alreadyExists("test-failure"));

var ctx = createContext();
assertThat(participantContextService.createParticipantContext(ctx))
.isFailed()
.satisfies(f -> Assertions.assertThat(f.getReason()).isEqualTo(ServiceFailure.Reason.CONFLICT));
verify(participantContextStore).create(any());
verifyNoMoreInteractions(vault, participantContextStore);

}

@Test
void getParticipantContext() {
var ctx = createContext();
when(participantContextStore.query(any())).thenReturn(StoreResult.success(Stream.of(ctx)));

assertThat(participantContextService.getParticipantContext("test-id"))
.isSucceeded()
.usingRecursiveComparison()
.isEqualTo(ctx);

verify(participantContextStore).query(any());
verifyNoMoreInteractions(vault);
}

@Test
void getParticipantContext_whenNotExists() {
when(participantContextStore.query(any())).thenReturn(StoreResult.success(Stream.of()));
assertThat(participantContextService.getParticipantContext("test-id"))
.isFailed()
.satisfies(f -> {
Assertions.assertThat(f.getReason()).isEqualTo(ServiceFailure.Reason.NOT_FOUND);
Assertions.assertThat(f.getFailureDetail()).isEqualTo("No ParticipantContext with ID 'test-id' was found.");
});

verify(participantContextStore).query(any());
verifyNoMoreInteractions(vault);
}


@Test
void getParticipantContext_whenStorageFails() {
when(participantContextStore.query(any())).thenReturn(StoreResult.notFound("foo bar"));
assertThat(participantContextService.getParticipantContext("test-id"))
.isFailed()
.satisfies(f -> {
Assertions.assertThat(f.getReason()).isEqualTo(ServiceFailure.Reason.NOT_FOUND);
Assertions.assertThat(f.getFailureDetail()).isEqualTo("foo bar");
});

verify(participantContextStore).query(any());
verifyNoMoreInteractions(vault);
}

@Test
void deleteParticipantContext() {
when(participantContextStore.deleteById(anyString())).thenReturn(StoreResult.success());
assertThat(participantContextService.deleteParticipantContext("test-id")).isSucceeded();

verify(participantContextStore).deleteById(anyString());
verifyNoMoreInteractions(vault);
}

@Test
void deleteParticipantContext_whenNotExists() {
when(participantContextStore.deleteById(any())).thenReturn(StoreResult.notFound("foo bar"));
assertThat(participantContextService.deleteParticipantContext("test-id"))
.isFailed()
.satisfies(f -> {
Assertions.assertThat(f.getReason()).isEqualTo(ServiceFailure.Reason.NOT_FOUND);
Assertions.assertThat(f.getFailureDetail()).isEqualTo("foo bar");
});

verify(participantContextStore).deleteById(anyString());
verifyNoMoreInteractions(vault);
}

@Test
void regenerateApiToken() {
when(participantContextStore.query(any())).thenReturn(StoreResult.success(Stream.of(createContext())));
when(vault.storeSecret(eq("test-alias"), anyString())).thenReturn(Result.success());

assertThat(participantContextService.regenerateApiToken("test-id")).isSucceeded().isNotNull();

verify(participantContextStore).query(any());
verify(vault).storeSecret(eq("test-alias"), argThat(s -> s.length() >= 64));
}

@Test
void regenerateApiToken_vaultFails() {
when(participantContextStore.query(any())).thenReturn(StoreResult.success(Stream.of(createContext())));
when(vault.storeSecret(eq("test-alias"), anyString())).thenReturn(Result.failure("test failure"));

assertThat(participantContextService.regenerateApiToken("test-id")).isFailed().detail().isEqualTo("Could not store new API token: test failure.");

verify(participantContextStore).query(any());
verify(vault).storeSecret(eq("test-alias"), anyString());
}

@Test
void regenerateApiToken_whenNotFound() {
when(participantContextStore.query(any())).thenReturn(StoreResult.success(Stream.of()));

assertThat(participantContextService.regenerateApiToken("test-id")).isFailed().detail().isEqualTo("No ParticipantContext with ID 'test-id' was found.");

verify(participantContextStore).query(any());
verifyNoMoreInteractions(participantContextStore, vault);
}

private ParticipantContext createContext() {
return ParticipantContext.Builder.newInstance()
.participantId("test-id")
.state(ParticipantContextState.CREATED)
.apiTokenAlias("test-alias")
.build();
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ include(":spi:identity-hub-did-spi")
// core modules
include(":core:identity-hub-api")
include(":core:identity-hub-credentials")
include(":core:identity-hub-participants")
include(":core:identity-hub-did")

// extension modules
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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
*
*/

package org.eclipse.edc.identityhub.spi;

/**
* Generates a random string, e.g. a UUID. Actual production implementations sould be more sophisticated, e.g. using seeds/salts and {@link java.security.SecureRandom}
*/
@FunctionalInterface
public interface RandomStringGenerator {
String generate();
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface ParticipantContextStore {
* Queries the store for ParticipantContexts based on the given query specification.
*
* @param querySpec The {@link QuerySpec} indicating the criteria for the query.
* @return A {@link StoreResult} object containing a list of {@link ParticipantContext} objects that match the query.
* @return A {@link StoreResult} object containing a list of {@link ParticipantContext} objects that match the query. If none are found, returns an empty stream.
*/
StoreResult<Stream<ParticipantContext>> query(QuerySpec querySpec);

Expand Down

0 comments on commit 2baa5f5

Please sign in to comment.