Skip to content

Commit

Permalink
feat(api): enable authentication for control-api (#4197)
Browse files Browse the repository at this point in the history
* feat(api): enable authentication for control-api

* pr remarks
  • Loading branch information
ndr-brt authored May 22, 2024
1 parent b929b37 commit b39e532
Show file tree
Hide file tree
Showing 47 changed files with 516 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private StatusResult<DataFlow> stop(String dataFlowId) {
private StatusResult<DataFlow> stop(String dataFlowId, String reason) {
var result = store.findByIdAndLease(dataFlowId);
if (result.failed()) {
return StatusResult.from(result).map(it -> null);
return StatusResult.from(result).mapFailure();
}

var dataFlow = result.getContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

package org.eclipse.edc.api;

import org.eclipse.edc.api.auth.spi.AllPassAuthenticationService;
import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.ApiAuthenticationRegistryImpl;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

/**
* Provides default service implementations for fallback
Expand All @@ -34,10 +33,9 @@ public String name() {
return NAME;
}

@Provider(isDefault = true)
public AuthenticationService authenticationService(ServiceExtensionContext context) {
context.getMonitor().warning("No AuthenticationService registered, an all-pass implementation will be used, not suitable for production environments");
return new AllPassAuthenticationService();
@Provider
public ApiAuthenticationRegistry apiAuthenticationRegistry() {
return new ApiAuthenticationRegistryImpl();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 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
Expand All @@ -8,11 +8,13 @@
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - Initial implementation
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.api.auth.spi;
package org.eclipse.edc.api.auth;

import org.eclipse.edc.api.auth.spi.AuthenticationService;

import java.util.List;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.api.auth;

import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;

public class ApiAuthenticationRegistryImpl implements ApiAuthenticationRegistry {

private static final AuthenticationService ALL_PASS = new AllPassAuthenticationService();
private final Map<String, AuthenticationService> services = new HashMap<>();

public ApiAuthenticationRegistryImpl() {
}

@Override
public void register(String context, AuthenticationService service) {
services.put(context, service);
}

@Override
public @NotNull AuthenticationService resolve(String context) {
return services.getOrDefault(context, ALL_PASS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.api.auth;

import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

class ApiAuthenticationRegistryImplTest {

private final ApiAuthenticationRegistryImpl registry = new ApiAuthenticationRegistryImpl();

@Test
void shouldResolveRegisteredService() {
AuthenticationService service = mock();
registry.register("context", service);

var actual = registry.resolve("context");

assertThat(actual).isSameAs(service);
}

@Test
void shouldReturnAllPass_whenNoServiceRegistered() {
var service = registry.resolve("context");

assertThat(service).isInstanceOf(AllPassAuthenticationService.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@

package org.eclipse.edc.connector.api.control.configuration;

import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter;
import org.eclipse.edc.api.auth.spi.ControlClientAuthenticationProvider;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.eclipse.edc.connector.controlplane.transfer.spi.callback.ControlApiUrl;
import org.eclipse.edc.jsonld.spi.JsonLd;
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;
Expand All @@ -34,6 +38,7 @@
import org.eclipse.edc.web.spi.configuration.WebServiceSettings;

import java.net.URI;
import java.util.Collections;

import static java.lang.String.format;
import static org.eclipse.edc.jsonld.spi.Namespaces.DSPACE_PREFIX;
Expand All @@ -54,6 +59,7 @@ public class ControlApiConfigurationExtension implements ServiceExtension {

@Setting(value = "Configures endpoint for reaching the Control API. If it's missing it defaults to the hostname configuration.")
public static final String CONTROL_API_ENDPOINT = "edc.control.endpoint";

public static final String CONTROL_CONTEXT_ALIAS = "control";
private static final String WEB_SERVICE_NAME = "Control API";
private static final int DEFAULT_CONTROL_API_PORT = 9191;
Expand All @@ -72,18 +78,16 @@ public class ControlApiConfigurationExtension implements ServiceExtension {
private WebServer webServer;
@Inject
private WebServiceConfigurer configurator;

@Inject
private WebService webService;

@Inject
private Hostname hostname;

@Inject
private JsonLd jsonLd;

@Inject
private TypeManager typeManager;
@Inject
private ApiAuthenticationRegistry authenticationRegistry;

@Override
public String name() {
Expand All @@ -93,18 +97,25 @@ public String name() {
@Override
public void initialize(ServiceExtensionContext context) {
var config = configurator.configure(context, webServer, SETTINGS);
var callbackAddress = controlApiUrl(context, config);
var jsonLdMapper = typeManager.getMapper(JSON_LD);
context.registerService(ControlApiConfiguration.class, new ControlApiConfiguration(config));
context.registerService(ControlApiUrl.class, callbackAddress);
context.registerService(ControlApiUrl.class, controlApiUrl(context, config));

jsonLd.registerNamespace(ODRL_PREFIX, ODRL_SCHEMA, CONTROL_SCOPE);
jsonLd.registerNamespace(DSPACE_PREFIX, DSPACE_SCHEMA, CONTROL_SCOPE);

var authenticationRequestFilter = new AuthenticationRequestFilter(authenticationRegistry, "control-api");
webService.registerResource(SETTINGS.getContextAlias(), authenticationRequestFilter);

webService.registerResource(SETTINGS.getContextAlias(), new ObjectMapperProvider(jsonLdMapper));
webService.registerResource(SETTINGS.getContextAlias(), new JerseyJsonLdInterceptor(jsonLd, jsonLdMapper, CONTROL_SCOPE));
}

@Provider(isDefault = true)
public ControlClientAuthenticationProvider controlClientAuthenticationProvider() {
return Collections::emptyMap;
}

private ControlApiUrl controlApiUrl(ServiceExtensionContext context, WebServiceConfiguration config) {
var callbackAddress = context.getSetting(CONTROL_API_ENDPOINT, format("http://%s:%s%s", hostname.get(), config.getPort(), config.getPath()));
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@

package org.eclipse.edc.connector.api.control.configuration;

import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter;
import org.eclipse.edc.connector.controlplane.transfer.spi.callback.ControlApiUrl;
import org.eclipse.edc.junit.extensions.DependencyInjectionExtension;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.system.Hostname;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.configuration.ConfigFactory;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.WebServiceConfiguration;
import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -33,13 +35,16 @@
import static org.eclipse.edc.connector.api.control.configuration.ControlApiConfigurationExtension.CONTROL_CONTEXT_ALIAS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(DependencyInjectionExtension.class)
public class ControlApiConfigurationExtensionTest {

private final WebServiceConfigurer configurator = mock();
private final WebService webService = mock();

private final WebServiceConfiguration webServiceConfiguration = WebServiceConfiguration.Builder.newInstance()
.contextAlias(CONTROL_CONTEXT_ALIAS)
Expand All @@ -51,6 +56,7 @@ public class ControlApiConfigurationExtensionTest {
void setUp(ServiceExtensionContext context) {
context.registerService(WebServiceConfigurer.class, configurator);
context.registerService(Hostname.class, () -> "localhost");
context.registerService(WebService.class, webService);

when(configurator.configure(any(), any(), any())).thenReturn(webServiceConfiguration);
}
Expand Down Expand Up @@ -94,6 +100,12 @@ void initialize_withInvalidEndpoint(ControlApiConfigurationExtension extension,
when(context.getSetting(eq(CONTROL_API_ENDPOINT), any())).thenReturn(endpoint);

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

@Test
void shouldRegisterAuthenticationFilter(ControlApiConfigurationExtension extension, ServiceExtensionContext context) {
extension.initialize(context);

verify(webService).registerResource(any(), isA(AuthenticationRequestFilter.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import jakarta.json.Json;
import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter;
import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.eclipse.edc.connector.api.management.configuration.transform.JsonObjectFromContractAgreementTransformer;
import org.eclipse.edc.connector.controlplane.transform.edc.from.JsonObjectFromAssetTransformer;
import org.eclipse.edc.connector.controlplane.transform.edc.to.JsonObjectToAssetTransformer;
Expand Down Expand Up @@ -77,7 +77,7 @@ public class ManagementApiConfigurationExtension implements ServiceExtension {
@Inject
private WebServer webServer;
@Inject
private AuthenticationService authenticationService;
private ApiAuthenticationRegistry authenticationRegistry;
@Inject
private WebServiceConfigurer configurator;
@Inject
Expand All @@ -99,7 +99,9 @@ public void initialize(ServiceExtensionContext context) {
var webServiceConfiguration = configurator.configure(context, webServer, SETTINGS);

context.registerService(ManagementApiConfiguration.class, new ManagementApiConfiguration(webServiceConfiguration));
webService.registerResource(webServiceConfiguration.getContextAlias(), new AuthenticationRequestFilter(authenticationService));

var authenticationFilter = new AuthenticationRequestFilter(authenticationRegistry, "management-api");
webService.registerResource(webServiceConfiguration.getContextAlias(), authenticationFilter);

jsonLd.registerNamespace(ODRL_PREFIX, ODRL_SCHEMA, MANAGEMENT_SCOPE);
var jsonLdMapper = typeManager.getMapper(JSON_LD);
Expand Down
4 changes: 2 additions & 2 deletions extensions/common/auth/auth-basic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ plugins {

dependencies {
api(project(":spi:common:auth-spi"))
api(project(":spi:common:core-spi"))
implementation(libs.jakarta.rsApi)

testImplementation(project(":core:common:junit"))
}


Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.eclipse.edc.api.auth.basic;

import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provides;
Expand All @@ -23,56 +24,43 @@
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import java.util.stream.Collectors;

import static java.lang.String.format;

/**
* Extension that registers an AuthenticationService that uses API Keys
*
* @deprecated this module is not supported anymore and it will be removed in the next iterations.
*/
@Provides(AuthenticationService.class)
@Extension(value = "Basic authentication")
@Deprecated(since = "0.6.5")
public class BasicAuthenticationExtension implements ServiceExtension {

@Setting
public static final String BASIC_AUTH = "edc.api.auth.basic.vault-keys";
@Setting(value = "Key-value object defining authentication credentials stored in the vault", type = "map", required = true)
static final String BASIC_AUTH = "edc.api.auth.basic.vault-keys";
@Inject
private Vault vault;
@Inject
private ApiAuthenticationRegistry authenticationRegistry;

@Override
public void initialize(ServiceExtensionContext context) {
var monitor = context.getMonitor();

monitor.warning("The 'auth-basic' module has been deprecated and it will removed in the next iterations.");

var credentials = context.getConfig(BASIC_AUTH)
.getRelativeEntries().entrySet().stream()
.map(entry -> new ConfigCredentials(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
.toList();

// Register basic authentication filter
if (!credentials.isEmpty()) {
var authService = new BasicAuthenticationService(vault, credentials, monitor);
context.registerService(AuthenticationService.class, authService);
authenticationRegistry.register("management-api", authService);
monitor.info(format("API Authentication: basic auth configured with %s credential(s)", credentials.size()));
} else {
monitor.warning("API Authentication: no basic auth credentials provided");
}
}

static class ConfigCredentials {
private final String username;
private final String vaultKey;

ConfigCredentials(String username, String vaultKey) {
this.username = username;
this.vaultKey = vaultKey;
}

public String getUsername() {
return username;
}

public String getVaultKey() {
return vaultKey;
}
}
}
Loading

0 comments on commit b39e532

Please sign in to comment.