Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): enable authentication for control-api #4197

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading