Skip to content

Commit

Permalink
feat: add a new management api for secrets (#4138)
Browse files Browse the repository at this point in the history
* feat: introduced API for managing secrets

* feat: introduced API for managing secrets - rebased

* feat: introduced API for managing secrets - fix SecretApiControllerTest tests

* feat: introduced API for managing secrets

* feat: introduced API for managing secrets - removed key from model, using id instead

* feat: introduced API for managing secrets - fix SecretApiControllerTest tests

* code review

* Code review

---------

Co-authored-by: Benjamin SCHOLTES <[email protected]>
  • Loading branch information
sbeuzit06 and bscholtes1A authored May 7, 2024
1 parent 873e86b commit 50f91f7
Show file tree
Hide file tree
Showing 36 changed files with 2,279 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ dependencies {
implementation(project(":spi:common:validator-spi"))
implementation(project(":spi:control-plane:control-plane-spi"))
implementation(project(":core:common:lib:util-lib"))
implementation(project(":spi:common:boot-spi"))
implementation(project(":spi:common:transaction-spi"))
implementation(project(":spi:control-plane:asset-spi"))
implementation(project(":spi:control-plane:secrets-spi"))
implementation(project(":spi:control-plane:transfer-data-plane-spi"))

implementation(libs.opentelemetry.instrumentation.annotations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.eclipse.edc.connector.controlplane.services.policydefinition.PolicyDefinitionServiceImpl;
import org.eclipse.edc.connector.controlplane.services.protocol.ProtocolTokenValidatorImpl;
import org.eclipse.edc.connector.controlplane.services.protocol.VersionProtocolServiceImpl;
import org.eclipse.edc.connector.controlplane.services.secret.SecretEventListener;
import org.eclipse.edc.connector.controlplane.services.secret.SecretServiceImpl;
import org.eclipse.edc.connector.controlplane.services.spi.asset.AssetService;
import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogProtocolService;
import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogService;
Expand All @@ -60,6 +62,8 @@
import org.eclipse.edc.connector.controlplane.transfer.spi.flow.FlowTypeExtractor;
import org.eclipse.edc.connector.controlplane.transfer.spi.observe.TransferProcessObservable;
import org.eclipse.edc.connector.controlplane.transfer.spi.store.TransferProcessStore;
import org.eclipse.edc.connector.secret.spi.observe.SecretObservableImpl;
import org.eclipse.edc.connector.spi.service.SecretService;
import org.eclipse.edc.policy.engine.spi.PolicyEngine;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
Expand All @@ -70,6 +74,7 @@
import org.eclipse.edc.spi.iam.IdentityService;
import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.telemetry.Telemetry;
Expand Down Expand Up @@ -97,7 +102,8 @@ public class ControlPlaneServicesExtension implements ServiceExtension {

@Inject
private AssetIndex assetIndex;

@Inject
private Vault vault;
@Inject
private ContractDefinitionStore contractDefinitionStore;

Expand Down Expand Up @@ -179,6 +185,13 @@ public AssetService assetService() {
return new AssetServiceImpl(assetIndex, contractNegotiationStore, transactionContext, assetObservable, dataAddressValidator);
}

@Provider
public SecretService secretService() {
var secretObservable = new SecretObservableImpl();
secretObservable.registerListener(new SecretEventListener(clock, eventRouter));
return new SecretServiceImpl(vault, secretObservable);
}

@Provider
public CatalogService catalogService() {
return new CatalogServiceImpl(dispatcher);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 Amadeus
*
* 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:
* Amadeus - Initial API and Implementation
*
*/

package org.eclipse.edc.connector.controlplane.services.secret;

import org.eclipse.edc.connector.secret.spi.event.SecretCreated;
import org.eclipse.edc.connector.secret.spi.event.SecretDeleted;
import org.eclipse.edc.connector.secret.spi.event.SecretEvent;
import org.eclipse.edc.connector.secret.spi.event.SecretUpdated;
import org.eclipse.edc.connector.secret.spi.observe.SecretListener;
import org.eclipse.edc.spi.event.EventEnvelope;
import org.eclipse.edc.spi.event.EventRouter;
import org.eclipse.edc.spi.types.domain.secret.Secret;

import java.time.Clock;

/**
* Listener responsible for creating and publishing events regarding Secret state changes
*/
public class SecretEventListener implements SecretListener {
private final Clock clock;
private final EventRouter eventRouter;

public SecretEventListener(Clock clock, EventRouter eventRouter) {
this.clock = clock;
this.eventRouter = eventRouter;
}

@Override
public void created(Secret secret) {
var event = SecretCreated.Builder.newInstance()
.secretId(secret.getId())
.build();

publish(event);
}

@Override
public void deleted(Secret secret) {
var event = SecretDeleted.Builder.newInstance()
.secretId(secret.getId())
.build();

publish(event);
}

@Override
public void updated(Secret secret) {
var event = SecretUpdated.Builder.newInstance()
.secretId(secret.getId())
.build();

publish(event);
}

private void publish(SecretEvent event) {
var envelope = EventEnvelope.Builder.newInstance()
.payload(event)
.at(clock.millis())
.build();
eventRouter.publish(envelope);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2024 Amadeus
*
* 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:
* Amadeus - Initial API and Implementation
*
*/

package org.eclipse.edc.connector.controlplane.services.secret;

import org.eclipse.edc.connector.secret.spi.observe.SecretObservable;
import org.eclipse.edc.connector.spi.service.SecretService;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.types.domain.secret.Secret;

import static java.util.Optional.ofNullable;
import static org.eclipse.edc.spi.result.ServiceResult.badRequest;
import static org.eclipse.edc.spi.result.ServiceResult.conflict;
import static org.eclipse.edc.spi.result.ServiceResult.notFound;
import static org.eclipse.edc.spi.result.ServiceResult.success;

public class SecretServiceImpl implements SecretService {
private final Vault vault;
private final SecretObservable observable;

public SecretServiceImpl(Vault vault, SecretObservable observable) {
this.vault = vault;
this.observable = observable;
}

@Override
public Secret findById(String secretId) {
return ofNullable(vault.resolveSecret(secretId))
.map(secretValue -> Secret.Builder.newInstance()
.value(secretValue)
.id(secretId)
.build())
.orElse(null);
}

@Override
public ServiceResult<Secret> create(Secret secret) {
var existing = findById(secret.getId());
if (existing != null) {
return conflict("Secret " + secret.getId() + " already exist");
}

return vault.storeSecret(secret.getId(), secret.getValue())
.onSuccess(unused -> observable.invokeForEach(l -> l.created(secret)))
.map(unused -> success(secret))
.orElse(failure -> badRequest(failure.getFailureDetail()));
}

@Override
public ServiceResult<Secret> delete(String secretKey) {
var existing = findById(secretKey);
if (existing == null) {
return notFound("Secret " + secretKey + " not found");
}

return vault.deleteSecret(secretKey)
.onSuccess(unused -> observable.invokeForEach(l -> l.deleted(existing)))
.map(unused -> success(existing))
.orElse(failure -> badRequest(failure.getFailureDetail()));
}

@Override
public ServiceResult<Secret> update(Secret secret) {
var existing = findById(secret.getId());
if (existing == null) {
return notFound("Secret " + secret.getId() + " not found");
}

return vault.storeSecret(secret.getId(), secret.getValue())
.onSuccess(unused -> observable.invokeForEach(l -> l.updated(secret)))
.map(unused -> success(secret))
.orElse(failure -> badRequest(failure.getFailureDetail()));
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2024 Amadeus
*
* 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:
* Amadeus - Initial API and Implementation
*
*/

package org.eclipse.edc.connector.controlplane.services.secret;

import org.eclipse.edc.connector.dataplane.selector.spi.store.DataPlaneInstanceStore;
import org.eclipse.edc.connector.secret.spi.event.SecretCreated;
import org.eclipse.edc.connector.secret.spi.event.SecretDeleted;
import org.eclipse.edc.connector.secret.spi.event.SecretEvent;
import org.eclipse.edc.connector.spi.service.SecretService;
import org.eclipse.edc.junit.extensions.EdcExtension;
import org.eclipse.edc.spi.event.EventRouter;
import org.eclipse.edc.spi.event.EventSubscriber;
import org.eclipse.edc.spi.iam.IdentityService;
import org.eclipse.edc.spi.protocol.ProtocolWebhook;
import org.eclipse.edc.spi.types.domain.secret.Secret;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.Map;

import static org.awaitility.Awaitility.await;
import static org.eclipse.edc.junit.matchers.EventEnvelopeMatcher.isEnvelopeOf;
import static org.eclipse.edc.util.io.Ports.getFreePort;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

@ExtendWith(EdcExtension.class)
public class SecretEventDispatchTest {

private final EventSubscriber eventSubscriber = mock(EventSubscriber.class);

@BeforeEach
void setUp(EdcExtension extension) {
extension.registerServiceMock(ProtocolWebhook.class, mock(ProtocolWebhook.class));
extension.registerServiceMock(DataPlaneInstanceStore.class, mock(DataPlaneInstanceStore.class));
extension.registerServiceMock(IdentityService.class, mock());
extension.setConfiguration(Map.of(
"web.http.port", String.valueOf(getFreePort()),
"web.http.path", "/api"
));
}

@Test
void shouldDispatchEventsOnSecretCreationAndDeletion(SecretService service, EventRouter eventRouter) {
eventRouter.register(SecretEvent.class, eventSubscriber);
var secret = Secret.Builder.newInstance().id("secretId").value("secret-value").build();

var result = service.create(secret);
await().untilAsserted(() -> {
verify(eventSubscriber).on(argThat(isEnvelopeOf(SecretCreated.class)));
});


service.delete(secret.getId());
await().untilAsserted(() -> {
verify(eventSubscriber).on(argThat(isEnvelopeOf(SecretDeleted.class)));
});
}
}
Loading

0 comments on commit 50f91f7

Please sign in to comment.