Skip to content

Commit

Permalink
chore: align activate-keypair operation with DR (#453)
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger authored Sep 11, 2024
1 parent 64cc36c commit 5884306
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairObservable;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted;
import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
Expand Down Expand Up @@ -45,6 +46,9 @@ public class KeyPairServiceExtension implements ServiceExtension {
private Clock clock;
@Inject
private TransactionContext transactionContext;
@Inject
private ParticipantContextStore participantContextService;


private KeyPairObservable observable;

Expand All @@ -55,7 +59,7 @@ public String name() {

@Provider
public KeyPairService createParticipantService(ServiceExtensionContext context) {
var service = new KeyPairServiceImpl(keyPairResourceStore, vault, context.getMonitor().withPrefix("KeyPairService"), keyPairObservable(), transactionContext);
var service = new KeyPairServiceImpl(keyPairResourceStore, vault, context.getMonitor().withPrefix("KeyPairService"), keyPairObservable(), transactionContext, participantContextService);
eventRouter.registerSync(ParticipantContextDeleted.class, service);
return service;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextCreated;
import org.eclipse.edc.identityhub.spi.participantcontext.events.ParticipantContextDeleted;
import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource;
import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore;
import org.eclipse.edc.security.token.jwt.CryptoConverter;
import org.eclipse.edc.spi.event.Event;
import org.eclipse.edc.spi.event.EventEnvelope;
Expand All @@ -40,31 +43,44 @@
import org.jetbrains.annotations.Nullable;

import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState.ACTIVATED;
import static org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState.CREATED;

public class KeyPairServiceImpl implements KeyPairService, EventSubscriber {
private final KeyPairResourceStore keyPairResourceStore;
private final Vault vault;
private final Monitor monitor;
private final KeyPairObservable observable;
private final TransactionContext transactionContext;
private final ParticipantContextStore participantContextService;

public KeyPairServiceImpl(KeyPairResourceStore keyPairResourceStore, Vault vault, Monitor monitor, KeyPairObservable observable, TransactionContext transactionContext) {
public KeyPairServiceImpl(KeyPairResourceStore keyPairResourceStore, Vault vault, Monitor monitor, KeyPairObservable observable, TransactionContext transactionContext, ParticipantContextStore participantContextService) {
this.keyPairResourceStore = keyPairResourceStore;
this.vault = vault;
this.monitor = monitor;
this.observable = observable;
this.transactionContext = transactionContext;
this.participantContextService = participantContextService;
}

@Override
public ServiceResult<Void> addKeyPair(String participantId, KeyDescriptor keyDescriptor, boolean makeDefault) {

return transactionContext.execute(() -> {

var result = checkParticipantState(participantId, ACTIVATED, CREATED);

if (result.failed()) {
return result.mapEmpty();
}

var key = generateOrGetKey(keyDescriptor);
if (key.failed()) {
return ServiceResult.badRequest(key.getFailureDetail());
Expand Down Expand Up @@ -187,6 +203,28 @@ public <E extends Event> void on(EventEnvelope<E> eventEnvelope) {
}
}

/**
* checks if the participant exists, and that its {@link ParticipantContext#state} flag matches either of the given states
*
* @param participantId the ParticipantContext ID of the participant context
* @param allowedStates a (possible empty) list of allowed states a participant may be in for a particular operation.
* @return {@link ServiceResult#success()} if the participant context exists, and is in one of the allowed states, a failure otherwise.
*/
private ServiceResult<Void> checkParticipantState(String participantId, ParticipantContextState... allowedStates) {
var result = ServiceResult.from(participantContextService.query(ParticipantContext.queryByParticipantId(participantId).build()))
.compose(list -> list.stream().findFirst()
.map(pc -> {
var state = pc.getStateAsEnum();
if (!Arrays.asList(allowedStates).contains(state)) {
return ServiceResult.badRequest("To add a key pair, the ParticipantContext with ID '%s' must be in state %s or %s but was %s."
.formatted(participantId, ACTIVATED, CREATED, state));
}
return ServiceResult.success();
})
.orElse(ServiceResult.notFound("No ParticipantContext with ID '%s' was found.".formatted(participantId))));
return result.mapEmpty();
}

private @NotNull ServiceResult<Void> activateKeyPair(KeyPairResource existingKeyPair) {
var allowedStates = List.of(KeyPairState.ACTIVATED.code(), KeyPairState.CREATED.code());
if (!allowedStates.contains(existingKeyPair.getState())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState;
import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContextState;
import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.transaction.spi.NoopTransactionContext;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
Expand All @@ -51,22 +56,30 @@

class KeyPairServiceImplTest {

public static final String PARTICIPANT_ID = "test-participant";
private final KeyPairResourceStore keyPairResourceStore = mock(i -> StoreResult.success());
private final Vault vault = mock();
private final KeyPairObservable observableMock = mock();
private final KeyPairServiceImpl keyPairService = new KeyPairServiceImpl(keyPairResourceStore, vault, mock(), observableMock, new NoopTransactionContext());
private final ParticipantContextStore participantContextServiceMock = mock();
private final KeyPairServiceImpl keyPairService = new KeyPairServiceImpl(keyPairResourceStore, vault, mock(), observableMock, new NoopTransactionContext(), participantContextServiceMock);


@BeforeEach
void setup() {
when(participantContextServiceMock.query(any(QuerySpec.class)))
.thenReturn(StoreResult.success(List.of(ParticipantContext.Builder.newInstance().participantId(PARTICIPANT_ID).apiTokenAlias("apitoken-alias").build())));
}

@ParameterizedTest(name = "make default: {0}")
@ValueSource(booleans = { true, false })
void addKeyPair_publicKeyGiven(boolean makeDefault) {

when(keyPairResourceStore.create(any())).thenReturn(success());
var key = createKey().publicKeyJwk(createJwk()).publicKeyPem(null).keyGeneratorParams(null).build();

assertThat(keyPairService.addKeyPair("some-participant", key, makeDefault)).isSucceeded();
assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, key, makeDefault)).isSucceeded();

verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() == makeDefault && kpr.getParticipantId().equals("some-participant")));
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() == makeDefault && kpr.getParticipantId().equals(PARTICIPANT_ID)));
// new key is set to active - expect an update in the DB
verify(keyPairResourceStore).update(argThat(kpr -> !kpr.getId().equals(key.getKeyId()) && kpr.getState() == KeyPairState.ACTIVATED.code()));
verify(observableMock, times(2)).invokeForEach(any());
Expand All @@ -83,11 +96,11 @@ void addKeyPair_shouldGenerate_storesInVault(boolean makeDefault) {
"curve", "Ed25519"
)).build();

assertThat(keyPairService.addKeyPair("some-participant", key, makeDefault)).isSucceeded();
assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, key, makeDefault)).isSucceeded();

verify(vault).storeSecret(eq(key.getPrivateKeyAlias()), anyString());
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() == makeDefault &&
kpr.getParticipantId().equals("some-participant") &&
kpr.getParticipantId().equals(PARTICIPANT_ID) &&
kpr.getState() == KeyPairState.ACTIVATED.code()));
// new key is set to active - expect an update in the DB
verify(keyPairResourceStore).update(argThat(kpr -> !kpr.getId().equals(key.getKeyId()) && kpr.getState() == KeyPairState.ACTIVATED.code()));
Expand All @@ -108,12 +121,12 @@ void addKeyPair_assertActiveState_whenKeyActive() {
"curve", "Ed25519"
)).build();

assertThat(keyPairService.addKeyPair("some-participant", key, true)).isSucceeded();
assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, key, true)).isSucceeded();

verify(vault).storeSecret(eq(key.getPrivateKeyAlias()), anyString());
//expect the query for other active keys at least once, if the new key is inactive
verify(keyPairResourceStore, never()).query(any());
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() && kpr.getParticipantId().equals("some-participant") && kpr.getState() == KeyPairState.ACTIVATED.code()));
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() && kpr.getParticipantId().equals(PARTICIPANT_ID) && kpr.getState() == KeyPairState.ACTIVATED.code()));
// new key is set to active - expect an update in the DB
verify(keyPairResourceStore).update(argThat(kpr -> !kpr.getId().equals(key.getKeyId()) && kpr.getState() == KeyPairState.ACTIVATED.code()));
verify(observableMock, times(2)).invokeForEach(any());
Expand All @@ -133,21 +146,41 @@ void addKeyPair_assertActiveState_whenKeyNotActive() {
"curve", "Ed25519"
)).build();

assertThat(keyPairService.addKeyPair("some-participant", key, true)).isSucceeded();
assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, key, true)).isSucceeded();

verify(vault).storeSecret(eq(key.getPrivateKeyAlias()), anyString());
//expect the query for other active keys at least once, if the new key is inactive
verify(keyPairResourceStore, times(1)).query(any());
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() && kpr.getParticipantId().equals("some-participant") && kpr.getState() == KeyPairState.CREATED.code()));
verify(keyPairResourceStore).create(argThat(kpr -> kpr.isDefaultPair() && kpr.getParticipantId().equals(PARTICIPANT_ID) && kpr.getState() == KeyPairState.CREATED.code()));
verify(observableMock, times(1)).invokeForEach(any());
verifyNoMoreInteractions(keyPairResourceStore, vault, observableMock);
}

@Test
void addKeyPair_participantNotFound() {
// can be implemented once events are used https://github.com/eclipse-edc/IdentityHub/issues/232
when(participantContextServiceMock.query(any(QuerySpec.class))).thenReturn(StoreResult.success(List.of()));

assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, createKey().build(), false)).isFailed()
.detail().isEqualTo("No ParticipantContext with ID '%s' was found.".formatted(PARTICIPANT_ID));
}


@Test
void addKeyPair_whenParticipantDeactivated_shouldFail() {
var pc = ParticipantContext.Builder.newInstance()
.participantId(PARTICIPANT_ID)
.apiTokenAlias("apitoken-alias")
.state(ParticipantContextState.DEACTIVATED)
.build();
when(participantContextServiceMock.query(any(QuerySpec.class))).thenReturn(StoreResult.success(List.of(pc)));

assertThat(keyPairService.addKeyPair(PARTICIPANT_ID, createKey().build(), false))
.isFailed()
.detail()
.isEqualTo("To add a key pair, the ParticipantContext with ID '%s' must be in state ACTIVATED or CREATED but was DEACTIVATED.".formatted(PARTICIPANT_ID));
}


@Test
void rotateKeyPair_withNewKey() {
var oldId = "old-id";
Expand Down Expand Up @@ -400,7 +433,7 @@ private KeyPairResource.Builder createKeyPairResource() {
.id(UUID.randomUUID().toString())
.keyId("test-key-1")
.privateKeyAlias("private-key-alias")
.participantId("test-participant")
.participantId(PARTICIPANT_ID)
.serializedPublicKey("this-is-a-pem-string")
.useDuration(Duration.ofDays(6).toMillis());
}
Expand Down
Loading

0 comments on commit 5884306

Please sign in to comment.