-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4f4ee20
commit 2baa5f5
Showing
7 changed files
with
345 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
97 changes: 97 additions & 0 deletions
97
...in/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()))); | ||
}); | ||
} | ||
} | ||
|
||
|
13 changes: 13 additions & 0 deletions
13
...icipants/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
# | ||
# |
199 changes: 199 additions & 0 deletions
199
...ava/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
...identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/RandomStringGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters