Skip to content

Commit

Permalink
feat: add API key override for Super-User (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger authored Feb 5, 2024
1 parent 3f7471c commit 3ab4aa0
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ dependencies {
implementation(libs.edc.core.jerseyproviders)
implementation(libs.jakarta.rsApi)

testImplementation(libs.edc.junit)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(<participantId>).<random-string>'");
}
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()));
}


Expand Down
Original file line number Diff line number Diff line change
@@ -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");

}

}

0 comments on commit 3ab4aa0

Please sign in to comment.