From 3ab4aa02db782bd4e5792fe783ab5a5e1a50a87c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:58:57 +0100 Subject: [PATCH] feat: add API key override for Super-User (#256) --- .../ParticipantContextExtension.java | 2 +- .../ParticipantContextServiceImpl.java | 6 +- .../ParticipantContextServiceImplTest.java | 2 +- .../build.gradle.kts | 2 + .../ManagementApiConfigurationExtension.java | 34 +++- ...nagementApiConfigurationExtensionTest.java | 148 ++++++++++++++++++ 6 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 extensions/api/identityhub-management-api-configuration/src/test/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtensionTest.java diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java index 91ebac4b2..f1fd34dc1 100644 --- a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextExtension.java @@ -59,7 +59,7 @@ public String name() { @Provider public ParticipantContextService createParticipantService() { - return new ParticipantContextServiceImpl(participantContextStore, vault, transactionContext, keyParserRegistry, participantContextObservable()); + return new ParticipantContextServiceImpl(participantContextStore, vault, transactionContext, participantContextObservable()); } @Provider diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java index b474d3e8e..b23ff7923 100644 --- a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java @@ -23,7 +23,6 @@ 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.KeyParserRegistry; import org.eclipse.edc.spi.security.Vault; import org.eclipse.edc.transaction.spi.TransactionContext; @@ -48,17 +47,14 @@ public class ParticipantContextServiceImpl implements ParticipantContextService private final Vault vault; private final TransactionContext transactionContext; private final ApiTokenGenerator tokenGenerator; - private final KeyParserRegistry keyParserRegistry; private final ParticipantContextObservable observable; - public ParticipantContextServiceImpl(ParticipantContextStore participantContextStore, Vault vault, TransactionContext transactionContext, - KeyParserRegistry registry, ParticipantContextObservable observable) { + public ParticipantContextServiceImpl(ParticipantContextStore participantContextStore, Vault vault, TransactionContext transactionContext, ParticipantContextObservable observable) { this.participantContextStore = participantContextStore; this.vault = vault; this.transactionContext = transactionContext; this.observable = observable; this.tokenGenerator = new ApiTokenGenerator(); - this.keyParserRegistry = registry; } @Override diff --git a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java index ac383e499..cc9e743a8 100644 --- a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java +++ b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java @@ -61,7 +61,7 @@ class ParticipantContextServiceImplTest { void setUp() { var keyParserRegistry = new KeyParserRegistryImpl(); keyParserRegistry.register(new PemParser(mock())); - participantContextService = new ParticipantContextServiceImpl(participantContextStore, vault, new NoopTransactionContext(), keyParserRegistry, observableMock); + participantContextService = new ParticipantContextServiceImpl(participantContextStore, vault, new NoopTransactionContext(), observableMock); } @ParameterizedTest(name = "isActive: {0}") diff --git a/extensions/api/identityhub-management-api-configuration/build.gradle.kts b/extensions/api/identityhub-management-api-configuration/build.gradle.kts index bb04a26ac..9345f3000 100644 --- a/extensions/api/identityhub-management-api-configuration/build.gradle.kts +++ b/extensions/api/identityhub-management-api-configuration/build.gradle.kts @@ -27,4 +27,6 @@ dependencies { implementation(libs.edc.core.jerseyproviders) implementation(libs.jakarta.rsApi) + testImplementation(libs.edc.junit) + } diff --git a/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java b/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java index 6749cabc4..1623a5fcd 100644 --- a/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java +++ b/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java @@ -25,6 +25,8 @@ 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.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.security.Vault; import org.eclipse.edc.spi.system.ServiceExtension; @@ -38,12 +40,16 @@ import java.util.Map; import java.util.function.Function; +import static java.util.Optional.ofNullable; import static org.eclipse.edc.identityhub.api.configuration.ManagementApiConfigurationExtension.NAME; @Extension(value = NAME) public class ManagementApiConfigurationExtension implements ServiceExtension { + @Setting(value = "Explicitly set the initial API key for the Super-User") + public static final String SUPERUSER_APIKEY_PROPERTY = "edc.ih.api.superuser.key"; public static final String NAME = "Management API Extension"; + public static final String SUPER_USER_PARTICIPANT_ID = "super-user"; private static final String MGMT_CONTEXT_ALIAS = "management"; private static final String DEFAULT_DID_PATH = "/api/management"; private static final int DEFAULT_DID_PORT = 8182; @@ -76,20 +82,36 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - // create super-user participantContextService.createParticipantContext(ParticipantManifest.Builder.newInstance() - .participantId("super-user") - .did("did:web:super-user") // doesn't matter, not intended for resolution + .participantId(SUPER_USER_PARTICIPANT_ID) + .did("did:web:%s".formatted(SUPER_USER_PARTICIPANT_ID)) // doesn't matter, not intended for resolution .active(true) .key(KeyDescriptor.Builder.newInstance() .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) - .keyId("super-user-key") - .privateKeyAlias("super-user-alias") + .keyId("%s-key".formatted(SUPER_USER_PARTICIPANT_ID)) + .privateKeyAlias("%s-alias".formatted(SUPER_USER_PARTICIPANT_ID)) .build()) .roles(List.of(ServicePrincipal.ROLE_ADMIN)) .build()) - .onSuccess(apiKey -> context.getMonitor().info("Created user 'super-user'. Please take a note . API Key: %s".formatted(apiKey))); + .onSuccess(generatedKey -> { + var monitor = context.getMonitor(); + var apiKey = ofNullable(context.getSetting(SUPERUSER_APIKEY_PROPERTY, null)) + .map(key -> { + if (!key.contains(".")) { + monitor.warning("Super-user key override: this key appears to have an invalid format, you may be unable to access some APIs. It must follow the structure: 'base64().'"); + } + participantContextService.getParticipantContext(SUPER_USER_PARTICIPANT_ID) + .onSuccess(pc -> vault.storeSecret(pc.getApiTokenAlias(), key) + .onSuccess(u -> monitor.debug("Super-user key override successful")) + .onFailure(f -> monitor.warning("Error storing API key in vault: %s".formatted(f.getFailureDetail())))) + .onFailure(f -> monitor.warning("Error overriding API key for '%s': %s".formatted(SUPER_USER_PARTICIPANT_ID, f.getFailureDetail()))); + return key; + }) + .orElse(generatedKey); + monitor.info("Created user 'super-user'. Please take note of the API Key: %s".formatted(apiKey)); + }) + .orElseThrow(f -> new EdcException("Error creating Super-User: " + f.getFailureDetail())); } diff --git a/extensions/api/identityhub-management-api-configuration/src/test/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtensionTest.java b/extensions/api/identityhub-management-api-configuration/src/test/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtensionTest.java new file mode 100644 index 000000000..383a1cb5a --- /dev/null +++ b/extensions/api/identityhub-management-api-configuration/src/test/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtensionTest.java @@ -0,0 +1,148 @@ +/* + * 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.api.configuration; + +import org.eclipse.edc.identityhub.spi.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +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.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +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; + +@ExtendWith(DependencyInjectionExtension.class) +class ManagementApiConfigurationExtensionTest { + + private final ParticipantContextService participantContextService = mock(); + private final Vault vault = mock(); + private final Monitor monitor = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(ParticipantContextService.class, participantContextService); + context.registerService(Vault.class, vault); + context.registerService(Monitor.class, monitor); + } + + @Test + void initialize_verifySuperUser(ManagementApiConfigurationExtension ext, + ServiceExtensionContext context) { + + when(participantContextService.createParticipantContext(any())).thenReturn(ServiceResult.success("some-key")); + + ext.initialize(context); + verify(participantContextService).createParticipantContext(any()); + verifyNoMoreInteractions(participantContextService); + } + + @Test + void initialize_failsToCreate(ManagementApiConfigurationExtension ext, ServiceExtensionContext context) { + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.badRequest("test-message")); + assertThatThrownBy(() -> ext.initialize(context)).isInstanceOf(EdcException.class); + verify(participantContextService).createParticipantContext(any()); + verifyNoMoreInteractions(participantContextService); + } + + @Test + void initialize_withApiKeyOverride(ManagementApiConfigurationExtension ext, + ServiceExtensionContext context) { + + + when(vault.storeSecret(any(), any())).thenReturn(Result.success()); + + var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; + when(context.getSetting(eq(ManagementApiConfigurationExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success("generated-api-key")); + when(participantContextService.getParticipantContext(eq("super-user"))) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + verify(participantContextService).createParticipantContext(any()); + verify(participantContextService).getParticipantContext(eq("super-user")); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verifyNoMoreInteractions(participantContextService, vault); + } + + @Test + void initialize_withInvalidKeyOverride(ManagementApiConfigurationExtension ext, + ServiceExtensionContext context) { + when(vault.storeSecret(any(), any())).thenReturn(Result.success()); + + var apiKeyOverride = "some-invalid-key"; + when(context.getSetting(eq(ManagementApiConfigurationExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success("generated-api-key")); + when(participantContextService.getParticipantContext(eq("super-user"))) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + verify(participantContextService).createParticipantContext(any()); + verify(participantContextService).getParticipantContext(eq("super-user")); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verify(monitor).warning(contains("this key appears to have an invalid format")); + verifyNoMoreInteractions(participantContextService, vault); + } + + @Test + void initialize_whenVaultReturnsFailure(ManagementApiConfigurationExtension ext, + ServiceExtensionContext context) { + when(vault.storeSecret(any(), any())).thenReturn(Result.failure("test-failure")); + + var apiKeyOverride = "c3VwZXItdXNlcgo=.asdfl;jkasdfl;kasdf"; + when(context.getSetting(eq(ManagementApiConfigurationExtension.SUPERUSER_APIKEY_PROPERTY), eq(null))) + .thenReturn(apiKeyOverride); + + when(participantContextService.createParticipantContext(any())) + .thenReturn(ServiceResult.success("generated-api-key")); + when(participantContextService.getParticipantContext(eq("super-user"))) + .thenReturn(ServiceResult.success(superUserContext().build())); + + ext.initialize(context); + verify(participantContextService).createParticipantContext(any()); + verify(participantContextService).getParticipantContext(eq("super-user")); + verify(vault).storeSecret(eq("super-user-apikey"), eq(apiKeyOverride)); + verify(monitor).warning(eq("Error storing API key in vault: test-failure")); + verifyNoMoreInteractions(participantContextService, vault); + } + + private ParticipantContext.Builder superUserContext() { + return ParticipantContext.Builder.newInstance() + .participantId("super-user") + .apiTokenAlias("super-user-apikey"); + + } + +} \ No newline at end of file