Skip to content

Commit

Permalink
feat: introduce auth configuration (#4321)
Browse files Browse the repository at this point in the history
* feat: introduce auth configuration

* PR remarks
  • Loading branch information
wolf4ood authored Jul 9, 2024
1 parent 9945eee commit c17a06b
Show file tree
Hide file tree
Showing 19 changed files with 689 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

package org.eclipse.edc.api;

import org.eclipse.edc.api.auth.ApiAuthenticationProviderRegistryImpl;
import org.eclipse.edc.api.auth.ApiAuthenticationRegistryImpl;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationProviderRegistry;
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;
Expand All @@ -37,5 +39,9 @@ public String name() {
public ApiAuthenticationRegistry apiAuthenticationRegistry() {
return new ApiAuthenticationRegistryImpl();
}


@Provider
public ApiAuthenticationProviderRegistry apiAuthenticationProviderRegistry() {
return new ApiAuthenticationProviderRegistryImpl();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.ApiAuthenticationProvider;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationProviderRegistry;

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

public class ApiAuthenticationProviderRegistryImpl implements ApiAuthenticationProviderRegistry {
private final Map<String, ApiAuthenticationProvider> providers = new HashMap<>();

@Override
public void register(String type, ApiAuthenticationProvider provider) {
providers.put(type, provider);
}

@Override
public ApiAuthenticationProvider resolve(String type) {
return providers.get(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.ApiAuthenticationProvider;
import org.junit.jupiter.api.Test;

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

class ApiAuthenticationProviderRegistryImplTest {

private final ApiAuthenticationProviderRegistryImpl registry = new ApiAuthenticationProviderRegistryImpl();

@Test
void shouldResolveRegisteredProvider() {
ApiAuthenticationProvider provider = mock();
registry.register("authType", provider);

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

assertThat(actual).isSameAs(provider);
}

}
25 changes: 25 additions & 0 deletions extensions/common/auth/auth-configuration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Authentication Configuration

This extension allows to secure a set of APIs grouped by a web context. It inspects
all `web.http.<context>` and if the authentication is configured it applies the `AuthenticationRequestFilter`
to the `<context>` with the chosen `AuthenticationService`. The chosen `AuthenticationService` is currently registered
in the `ApiAuthenticationRegistry`. This will be removed once the `ApiAuthenticationRegistry` will be refactored out.

## Configuration

| Key | Description | Mandatory |
|:--------------------------------|:-------------------------------------------------------------------------------------------|-----------|
| web.http.<context>.auth.type | The type of authentication to apply to the `<context>` | |
| web.http.<context>.auth.context | Override the name of the context in the `ApiAuthenticationRegistry` instead of `<context>` | |

Depending on the `web.http.<context>.auth.type` chosen, additional properties might be required in order to configure
the `AuthenticationService`.

Example of a complete configuration for a custom context with token based authentication

```properties
web.http.custom.path=/custom
web.http.custom.port=8081
web.http.custom.auth.type=tokenbased
web.http.custom.auth.key=apiKey
```
25 changes: 25 additions & 0 deletions extensions/common/auth/auth-configuration/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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
*
*/

plugins {
`java-library`
}

dependencies {
api(project(":spi:common:auth-spi"))

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


Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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.configuration;

import org.eclipse.edc.api.auth.spi.AuthenticationRequestFilter;
import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationProviderRegistry;
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.Setting;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.configuration.Config;
import org.eclipse.edc.web.spi.WebService;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.eclipse.edc.api.auth.configuration.ApiAuthenticationConfigurationExtension.NAME;
import static org.eclipse.edc.web.spi.configuration.WebServiceConfigurer.WEB_HTTP_PREFIX;

@Extension(NAME)
public class ApiAuthenticationConfigurationExtension implements ServiceExtension {

public static final String NAME = "Api Authentication Configuration Extension";

public static final String AUTH_KEY = "auth";
public static final String CONFIG_ALIAS = WEB_HTTP_PREFIX + ".<context>." + AUTH_KEY + ".";

@Setting(context = CONFIG_ALIAS, value = "The type of the authentication provider.", required = true)
public static final String TYPE_KEY = "type";

@Setting(context = CONFIG_ALIAS, value = "The api context where to apply the authentication. Default to the web <context>")
public static final String CONTEXT_KEY = "context";

private Map<String, Config> authConfiguration = new HashMap<>();

@Inject
private ApiAuthenticationProviderRegistry providerRegistry;

@Inject
private ApiAuthenticationRegistry authenticationRegistry;

@Inject
private WebService webService;


@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {
authConfiguration = context.getConfig(WEB_HTTP_PREFIX)
.partition().filter(config -> config.getString(AUTH_KEY + "." + TYPE_KEY, null) != null)
.map(cfg -> Map.entry(cfg.currentNode(), cfg.getConfig(AUTH_KEY)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public void prepare() {
for (var entry : authConfiguration.entrySet()) {
var webContext = entry.getKey();
var apiContext = Optional.ofNullable(entry.getValue().getString(CONTEXT_KEY, null)).orElse(webContext);
var serviceResult = configureService(entry.getValue());
if (serviceResult.failed()) {
throw new EdcException("Failed to configure authentication for context %s: %s".formatted(entry.getKey(), serviceResult.getFailureDetail()));
}
authenticationRegistry.register(apiContext, serviceResult.getContent());
var authenticationFilter = new AuthenticationRequestFilter(authenticationRegistry, apiContext);
webService.registerResource(entry.getKey(), authenticationFilter);
}
}

private Result<AuthenticationService> configureService(Config config) {
var type = config.getString(TYPE_KEY);
return Optional.ofNullable(providerRegistry.resolve(type))
.map(provider -> provider.provide(config))
.orElseGet(() -> Result.failure("Authentication provider for type %s not found".formatted(type)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#
# 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
#
#

org.eclipse.edc.api.auth.configuration.ApiAuthenticationConfigurationExtension
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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.configuration;

import org.eclipse.edc.api.auth.spi.ApiAuthenticationProvider;
import org.eclipse.edc.api.auth.spi.AuthenticationService;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationProviderRegistry;
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry;
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.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.util.Map;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.edc.web.spi.configuration.WebServiceConfigurer.WEB_HTTP_PREFIX;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
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 ApiAuthenticationConfigurationExtensionTest {

private final Monitor monitor = mock(Monitor.class);
private final ApiAuthenticationRegistry authenticationRegistry = mock();
private final ApiAuthenticationProviderRegistry providerRegistry = mock();

@BeforeEach
void setUp(ServiceExtensionContext context) {
when(monitor.withPrefix(anyString())).thenReturn(monitor);
when(context.getMonitor()).thenReturn(monitor);
context.registerService(ApiAuthenticationRegistry.class, authenticationRegistry);
context.registerService(ApiAuthenticationProviderRegistry.class, providerRegistry);
}

@Test
void prepare(ApiAuthenticationConfigurationExtension extension, ServiceExtensionContext context) {
var authType = "testAuth";
var config = ConfigFactory.fromMap(Map.of("test.auth.type", authType, "test.auth.custom", "custom"));
var provider = mock(ApiAuthenticationProvider.class);
var authentication = mock(AuthenticationService.class);
when(context.getConfig(WEB_HTTP_PREFIX)).thenReturn(config);
when(providerRegistry.resolve(authType)).thenReturn(provider);
when(provider.provide(any())).thenReturn(Result.success(authentication));

extension.initialize(context);
extension.prepare();

verify(providerRegistry).resolve(authType);
verify(provider).provide(any());
verify(authenticationRegistry).register("test", authentication);

}

@Test
void prepare_whenEmptyConfig(ApiAuthenticationConfigurationExtension extension, ServiceExtensionContext context) {
var authType = "testAuth";
var config = ConfigFactory.fromMap(Map.of());
var provider = mock(ApiAuthenticationProvider.class);
var authentication = mock(AuthenticationService.class);
when(context.getConfig(WEB_HTTP_PREFIX)).thenReturn(config);
when(providerRegistry.resolve(authType)).thenReturn(provider);
when(provider.provide(any())).thenReturn(Result.success(authentication));

extension.initialize(context);
extension.prepare();

verifyNoMoreInteractions(providerRegistry);
verifyNoMoreInteractions(provider);
verifyNoMoreInteractions(authenticationRegistry);

}

@Test
void prepare_whenNoProvider(ApiAuthenticationConfigurationExtension extension, ServiceExtensionContext context) {
var authType = "testAuth";
var config = ConfigFactory.fromMap(Map.of("test.auth.type", authType, "test.auth.custom", "custom"));
var provider = mock(ApiAuthenticationProvider.class);
when(context.getConfig(WEB_HTTP_PREFIX)).thenReturn(config);
when(providerRegistry.resolve(authType)).thenReturn(null);

extension.initialize(context);

assertThatThrownBy(extension::prepare).isInstanceOf(EdcException.class).hasMessageContaining(authType);

verify(providerRegistry).resolve(authType);
verifyNoMoreInteractions(provider);
verifyNoMoreInteractions(authenticationRegistry);

}

@Test
void prepare_whenProviderFails(ApiAuthenticationConfigurationExtension extension, ServiceExtensionContext context) {
var authType = "testAuth";
var config = ConfigFactory.fromMap(Map.of("test.auth.type", authType, "test.auth.custom", "custom"));
var provider = mock(ApiAuthenticationProvider.class);
when(context.getConfig(WEB_HTTP_PREFIX)).thenReturn(config);
when(providerRegistry.resolve(authType)).thenReturn(provider);
when(provider.provide(any())).thenReturn(Result.failure("Auth failure"));

extension.initialize(context);

assertThatThrownBy(extension::prepare).isInstanceOf(EdcException.class).hasMessageContaining("Auth failure");

verify(providerRegistry).resolve(authType);
verify(provider).provide(any());
verifyNoMoreInteractions(authenticationRegistry);

}

}
Loading

0 comments on commit c17a06b

Please sign in to comment.