Skip to content

Commit

Permalink
refactor: improve Public Key Resolver (#3799)
Browse files Browse the repository at this point in the history
* feat: improves PublicKeyResolver

* pr suggestions
  • Loading branch information
wolf4ood committed Jan 25, 2024
1 parent b23d1bc commit 40fbe08
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2024 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
*
*/

package org.eclipse.edc.connector.core;

import org.eclipse.edc.connector.core.security.LocalPublicKeyServiceImpl;
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.Provides;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.iam.LocalPublicKeyService;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.KeyParserRegistry;
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.system.configuration.Config;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import static java.util.function.Function.identity;
import static org.eclipse.edc.connector.core.SecurityDefaultServicesExtension.NAME;

@Extension(value = NAME)
@Provides(LocalPublicKeyService.class)
public class LocalPublicKeyDefaultExtension implements ServiceExtension {

public static final String NAME = "Local Public Key Default Extension";

public static final String EDC_PUBLIC_KEYS_PREFIX = "edc.iam.publickeys";

public static final String CONFIG_ALIAS = EDC_PUBLIC_KEYS_PREFIX + ".<pkAlias>.";

@Setting(context = CONFIG_ALIAS, value = "ID of the public key.", required = true)
public static final String ID_SUFFIX = "id";

@Setting(context = CONFIG_ALIAS, value = "Value of the public key. Multiple formats are supported, depending on the KeyParsers registered in the runtime")
public static final String VALUE_SUFFIX = "value";

@Setting(context = CONFIG_ALIAS, value = "Path to a file that holds the public key, e.g. a PEM file. Multiple formats are supported, depending on the KeyParsers registered in the runtime")
public static final String PATH_SUFFIX = "path";

@Inject
public KeyParserRegistry keyParserRegistry;

private Config keysConfiguration;

private LocalPublicKeyServiceImpl localPublicKeyService;
@Inject
private Vault vault;

@Provider(isDefault = true)
public LocalPublicKeyService localPublicKeyService() {
return localPublicKeyServiceImpl();
}

private LocalPublicKeyServiceImpl localPublicKeyServiceImpl() {
if (localPublicKeyService == null) {
localPublicKeyService = new LocalPublicKeyServiceImpl(vault, keyParserRegistry);
}
return localPublicKeyService;
}

@Override
public void initialize(ServiceExtensionContext context) {
keysConfiguration = context.getConfig(EDC_PUBLIC_KEYS_PREFIX);
}

@Override
public void prepare() {
if (keysConfiguration != null) {
var result = keysConfiguration.partition().map(this::readPublicKey)
.map(entry -> localPublicKeyServiceImpl().addRawKey(entry.getKey(), entry.getValue()))
.reduce(Result.success(), Result::merge);

result.orElseThrow((failure) -> new EdcException(failure.getFailureDetail()));
}
}

private Map.Entry<String, String> readPublicKey(Config config) {
var id = config.getString(ID_SUFFIX);
return readFrom(config, VALUE_SUFFIX, identity())
.or(() -> readFrom(config, PATH_SUFFIX, this::readFromPath))
.map(key -> Map.entry(id, key))
.orElseThrow(() -> new EdcException(""));
}

private String readFromPath(String path) {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Optional<String> readFrom(Config config, String setting, Function<String, String> mapper) {
return Optional.ofNullable(config.getString(setting, null))
.map(mapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@
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.spi.iam.PublicKeyResolver;
import org.eclipse.edc.spi.security.CertificateResolver;
import org.eclipse.edc.spi.security.KeyParserRegistry;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.security.VaultCertificateResolver;
import org.eclipse.edc.spi.security.VaultPrivateKeyResolver;
import org.eclipse.edc.spi.security.VaultPublicKeyResolver;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
Expand Down Expand Up @@ -61,11 +59,6 @@ public PrivateKeyResolver privateKeyResolver(ServiceExtensionContext context) {
return privateKeyResolver;
}

@Provider(isDefault = true)
public PublicKeyResolver createDefaultPublicKeyResolver(ServiceExtensionContext context) {
return new VaultPublicKeyResolver(keyParserRegistry(context), context.getConfig(), context.getMonitor().withPrefix("PublicKeyResolution"), vault);
}

@Provider(isDefault = true)
public CertificateResolver certificateResolver() {
return new VaultCertificateResolver(vault);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 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
*
*/

package org.eclipse.edc.connector.core.security;

import org.eclipse.edc.spi.iam.LocalPublicKeyService;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.KeyParserRegistry;
import org.eclipse.edc.spi.security.Vault;

import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* Implementation of {@link LocalPublicKeyService} which looks-up for the key by id
* first in the locally cached keys and as fallback in the vault.
*/
public class LocalPublicKeyServiceImpl implements LocalPublicKeyService {
private final Vault vault;

private final KeyParserRegistry registry;

private final Map<String, PublicKey> cachedKeys = new HashMap<>();

public LocalPublicKeyServiceImpl(Vault vault, KeyParserRegistry registry) {
this.vault = vault;
this.registry = registry;
}

@Override
public Result<PublicKey> resolveKey(String id) {
return resolveFromCache(id)
.map(Result::success)
.or(() -> resolveFromVault(id).map(this::parseKey))
.orElseGet(() -> Result.failure("No public key could be resolved for key-ID '%s'".formatted(id)));
}

private Optional<String> resolveFromVault(String id) {
return Optional.ofNullable(vault.resolveSecret(id));
}

private Optional<PublicKey> resolveFromCache(String id) {
return Optional.ofNullable(cachedKeys.get(id));
}

private Result<PublicKey> parseKey(String encodedKey) {
return registry.parse(encodedKey).compose(pk -> {
if (pk instanceof PublicKey publicKey) {
return Result.success(publicKey);
} else {
return Result.failure("The specified resource did not contain public key material.");
}
});
}

public Result<Void> addRawKey(String id, String rawKey) {
return parseKey(rawKey).onSuccess((pk) -> cachedKeys.put(id, pk)).mapTo();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
org.eclipse.edc.connector.core.CoreServicesExtension
org.eclipse.edc.connector.core.CoreDefaultServicesExtension
org.eclipse.edc.connector.core.SecurityDefaultServicesExtension
org.eclipse.edc.connector.core.LocalPublicKeyDefaultExtension
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 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
*
*/

package org.eclipse.edc.connector.core;

import org.eclipse.edc.connector.core.security.LocalPublicKeyServiceImpl;
import org.eclipse.edc.junit.extensions.DependencyInjectionExtension;
import org.eclipse.edc.junit.testfixtures.TestUtils;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.KeyParserRegistry;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.configuration.ConfigFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.security.PublicKey;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.edc.connector.core.LocalPublicKeyDefaultExtension.EDC_PUBLIC_KEYS_PREFIX;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(DependencyInjectionExtension.class)
class LocalPublicKeyDefaultExtensionTest {

private final KeyParserRegistry keyParserRegistry = mock();

@BeforeEach
void setUp(ServiceExtensionContext context) {
context.registerService(KeyParserRegistry.class, keyParserRegistry);
}

@Test
void localPublicKeyService(LocalPublicKeyDefaultExtension extension) {
assertThat(extension.localPublicKeyService()).isInstanceOf(LocalPublicKeyServiceImpl.class);
}

@Test
void localPublicKeyService_withValueConfig(LocalPublicKeyDefaultExtension extension, ServiceExtensionContext context) {

var keys = Map.of(
"key1.id", "key1",
"key1.value", "value");

when(keyParserRegistry.parse("value")).thenReturn(Result.success(mock(PublicKey.class)));
when(context.getConfig(EDC_PUBLIC_KEYS_PREFIX)).thenReturn(ConfigFactory.fromMap(keys));
var localPublicKeyService = extension.localPublicKeyService();
extension.initialize(context);
extension.prepare();

assertThat(localPublicKeyService.resolveKey("key1")).isSucceeded();
}

@Test
void localPublicKeyService_withPathConfig(LocalPublicKeyDefaultExtension extension, ServiceExtensionContext context) {
var path = TestUtils.getResource("rsa_2048.pem");
var value = TestUtils.getResourceFileContentAsString("rsa_2048.pem");
var keys = Map.of(
"key1.id", "key1",
"key1.path", path.getPath());

when(keyParserRegistry.parse(value)).thenReturn(Result.success(mock(PublicKey.class)));
when(context.getConfig(EDC_PUBLIC_KEYS_PREFIX)).thenReturn(ConfigFactory.fromMap(keys));
var localPublicKeyService = extension.localPublicKeyService();
extension.initialize(context);
extension.prepare();

assertThat(localPublicKeyService.resolveKey("key1")).isSucceeded();
}

@Test
void localPublicKeyService_shouldRaiseException_withoutValueOrPath(LocalPublicKeyDefaultExtension extension, ServiceExtensionContext context) {
var keys = Map.of(
"key1.id", "key1");

when(context.getConfig(EDC_PUBLIC_KEYS_PREFIX)).thenReturn(ConfigFactory.fromMap(keys));
extension.initialize(context);

assertThatThrownBy(() -> extension.prepare()).isInstanceOf(EdcException.class);
}

}
Loading

0 comments on commit 40fbe08

Please sign in to comment.