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

Feature: Provide authentication mechanisms for AAS services/registries #133

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@

version: 2
updates:
- package-ecosystem: "gradle"
directory: "/client"
schedule:
interval: "daily"

- package-ecosystem: "gradle"
directory: "/data-plane-aas"
schedule:
interval: "daily"

- package-ecosystem: "gradle"
directory: "/edc-extension4aas"
schedule:
Expand All @@ -15,6 +25,11 @@ updates:
schedule:
interval: "daily"

- package-ecosystem: "gradle"
directory: "/public-api-management"
schedule:
interval: "daily"

# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
Expand Down
6 changes: 5 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Current development version

Compatibility: **Eclipse Dataspace Connector v0.8.1**
Compatibility: **Eclipse Dataspace Connector v0.8.1, v0.9.0**

**New Features**

Expand All @@ -14,6 +14,10 @@ Compatibility: **Eclipse Dataspace Connector v0.8.1**
* When a contract is negotiated for one of those elements, the endpoint provided by the
shell-/submodel-descriptor
is used as data source for the data transfer
* Add AAS Authentication schemes
* If an external AAS service/registry needs authentication, this can be configured when registering the
service/registry at the extension
* example: `{ "type":"basic", "username": "admin", "password": "administrator" }`

**Bugfixes**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ public Response negotiateContract(@QueryParam("providerUrl") URL counterPartyUrl
@QueryParam("assetId") String assetId,
DataAddress dataAddress) {
monitor.info("POST /%s".formatted(NEGOTIATE_PATH));
if (Objects.isNull(counterPartyUrl) || Objects.isNull(counterPartyId) || Objects.isNull(assetId)) {
if (counterPartyUrl == null || counterPartyId == null || assetId == null ||
assetId.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(MISSING_QUERY_PARAMETER_MESSAGE.formatted("providerUrl, counterPartyId, assetId")).build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
class PolicyService {

private static final String CATALOG_RETRIEVAL_FAILURE_MSG = "Catalog by provider %s couldn't be retrieved: %s";
public static final String AMBIGUOUS_OR_NULL_MESSAGE = "Multiple or no policyDefinitions were received for assetId %s!";
private final CatalogService catalogService;
private final TypeTransformerRegistry transformer;

Expand Down Expand Up @@ -114,7 +115,7 @@ Dataset getDatasetForAssetId(@NotNull String counterPartyId, @NotNull URL counte

if (Objects.isNull(datasets) || datasets.size() != 1) {
throw new AmbiguousOrNullException(
format("Multiple or no policyDefinitions were found for assetId %s!",
format(AMBIGUOUS_OR_NULL_MESSAGE,
assetId));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import static de.fraunhofer.iosb.client.policy.PolicyService.AMBIGUOUS_OR_NULL_MESSAGE;
import static java.lang.String.format;
import static org.eclipse.edc.protocol.dsp.http.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP;
import static org.eclipse.edc.spi.query.Criterion.criterion;
Expand Down Expand Up @@ -128,7 +129,7 @@ void getDatasetNoDatasetsTest() throws InterruptedException {
policyService.getDatasetForAssetId("test-counter-party-id", testUrl, "test-asset-id");
fail(); // Should throw exception
} catch (AmbiguousOrNullException expected) {
assertEquals(format("Multiple or no policyDefinitions were found for assetId %s!", "test-asset-id"), expected.getMessage());
assertEquals(AMBIGUOUS_OR_NULL_MESSAGE.formatted("test-asset-id"), expected.getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ public Response send(@NotNull AasDataAddress aasDataAddress, @NotNull Part part)

private Response send(AasDataAddress aasDataAddress, byte[] bytes, String mediaType) throws IOException {
var requestUrlBuilder = HttpUrl.get(aasDataAddress.getBaseUrl()).newBuilder();
if (!aasDataAddress.referenceChainAsPath().isEmpty()) {
requestUrlBuilder.addPathSegments(aasDataAddress.referenceChainAsPath());

var requestPath = aasDataAddress.getPath();

if (!requestPath.isEmpty()) {
// Remove leading forward slash
requestPath = requestPath.startsWith("/") ? requestPath.substring(1) : requestPath;
requestUrlBuilder.addPathSegments(requestPath);
}

var requestUrl = requestUrlBuilder.build().url();
var requestUrl = requestUrlBuilder.build().url();
var requestBody = new AasTransferRequestBody(bytes, mediaType);

var request = new Request.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public DataSink createSink(DataFlowStartMessage request) {
.monitor(monitor)
.aasDataAddress(dataAddress)
.build();

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import de.fraunhofer.iosb.model.aas.AasProvider;
import de.fraunhofer.iosb.util.Encoder;
import org.eclipse.digitaltwin.aas4j.v3.model.Reference;
import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.types.domain.DataAddress;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -44,12 +46,13 @@
@JsonDeserialize(builder = DataAddress.Builder.class)
public class AasDataAddress extends DataAddress {

public static final String REFERENCE_CHAIN = "referenceChain";
public static final String BASE_URL = "https://w3id.org/edc/v0.0.1/ns/baseUrl";

private static final String ADDITIONAL_HEADER = "header:";
private static final String METHOD = "method";
private static final String QUERY_PARAMS = "queryParams";
private static final String PROVIDER = "AAS-Provider";
private static final String REFERENCE_CHAIN = "referenceChain";
private static final String PATH = "PATH";

private AasDataAddress() {
super();
Expand All @@ -58,7 +61,19 @@ private AasDataAddress() {

@JsonIgnore
public String getBaseUrl() {
return getStringProperty(BASE_URL);
return hasProvider() ? getProvider().getAccessUrl().toString() : getStringProperty(BASE_URL);
}

private AasProvider getProvider() {
Object provider = super.getProperties().get(PROVIDER);
if (provider instanceof AasProvider) {
return (AasProvider) provider;
}
throw new EdcException(new IllegalStateException("Provider not set correctly: %s".formatted(provider)));
}

private boolean hasProvider() {
return getProperties().get(PROVIDER) != null;
}

@JsonIgnore
Expand All @@ -68,29 +83,37 @@ public String getMethod() {

@JsonIgnore
public Map<String, String> getAdditionalHeaders() {
return getProperties().entrySet().stream()
// First get authentication headers from aas provider, then additional ones
Map<String, String> headers = hasProvider() ? getProvider().getHeaders() : new HashMap<>();
headers.putAll(getProperties().entrySet().stream()
.filter(entry -> entry.getKey().startsWith(ADDITIONAL_HEADER))
.collect(toMap(headerName -> headerName.getKey().replace(ADDITIONAL_HEADER, ""), headerValue -> (String) headerValue.getValue()));
.collect(toMap(headerName -> headerName.getKey().replace(ADDITIONAL_HEADER, ""),
headerValue -> (String) headerValue.getValue())));
return headers;
}

/**
* Builds and returns the HTTP URL path required to access this AAS data at the AAS service.
* If an explicit path is available, return this path. Else, return the following:
* <p>
* build and returns the HTTP URL path required to access this AAS data at the AAS service.
* Example: ReferenceChain: [Submodel x, SubmodelElementCollection y, SubmodelElement z]
* --> path: submodels/base64(x)/submodel-elements/y.z
*
* @return Path correlating to reference chain stored in this DataAddress (no leading '/').
* @return Explicitly defined path or path correlating to reference chain stored in this DataAddress (no leading '/').
*/
public String referenceChainAsPath() {
public String getPath() {
return getStringProperty(PATH, referenceChainAsPath());
}

private String referenceChainAsPath() {
StringBuilder urlBuilder = new StringBuilder();

for (var key : getReferenceChain().getKeys()) {

switch (key.getType()) {
case ASSET_ADMINISTRATION_SHELL ->
urlBuilder.append("shells/").append(Encoder.encodeBase64(key.getValue()));
case ASSET_ADMINISTRATION_SHELL -> urlBuilder.append("shells/").append(Encoder.encodeBase64(key.getValue()));
case SUBMODEL -> urlBuilder.append("submodels/").append(Encoder.encodeBase64(key.getValue()));
case CONCEPT_DESCRIPTION ->
urlBuilder.append("concept-descriptions/").append(Encoder.encodeBase64(key.getValue()));
case CONCEPT_DESCRIPTION -> urlBuilder.append("concept-descriptions/").append(Encoder.encodeBase64(key.getValue()));
case SUBMODEL_ELEMENT, SUBMODEL_ELEMENT_COLLECTION, SUBMODEL_ELEMENT_LIST -> {
if (urlBuilder.indexOf("/submodel-elements/") == -1) {
urlBuilder.append("/submodel-elements/");
Expand All @@ -99,8 +122,7 @@ public String referenceChainAsPath() {
}
urlBuilder.append(key.getValue());
}
default ->
throw new EdcException(new IllegalStateException(format("Element type not recognized in AasDataAddress: %s", key.getType())));
default -> throw new EdcException(new IllegalStateException(format("Element type not recognized in AasDataAddress: %s", key.getType())));
}
}

Expand Down Expand Up @@ -134,13 +156,18 @@ public static Builder newInstance() {
return new Builder();
}

public Builder aasProvider(AasProvider provider) {
this.property(PROVIDER, provider);
return this;
}

public Builder baseUrl(String baseUrl) {
this.property(BASE_URL, baseUrl);
return this;
}

public Builder queryParams(String queryParams) {
this.property(QUERY_PARAMS, queryParams);
public Builder path(String path) {
this.property(PATH, path);
return this;
}

Expand All @@ -157,7 +184,10 @@ public Builder referenceChain(Reference referenceChain) {
}

public Builder copyFrom(DataAddress other) {
(Optional.ofNullable(other).map(DataAddress::getProperties).orElse(Collections.emptyMap())).forEach(this::property);
(Optional.ofNullable(other)
.map(DataAddress::getProperties)
.orElse(Collections.emptyMap()))
.forEach(this::property);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige
* Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten
* Forschung e.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.fraunhofer.iosb.model.aas;

import com.fasterxml.jackson.annotation.JsonAlias;
import de.fraunhofer.iosb.model.aas.auth.AuthenticationMethod;
import de.fraunhofer.iosb.model.aas.auth.impl.NoAuth;
import de.fraunhofer.iosb.model.aas.net.AasAccessUrl;

import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public abstract class AasProvider {

public static final String AAS_V3_PREFIX = "/api/v3.0";

@JsonAlias("url")
private final AasAccessUrl url;
@JsonAlias("auth")
private final AuthenticationMethod authentication;

public AasProvider(AasAccessUrl url) {
this.url = url;
this.authentication = new NoAuth();
}

public AasProvider(AasAccessUrl url, AuthenticationMethod authentication) {
this.url = url;
this.authentication = authentication;
}

protected AasProvider(AasProvider from) {
this.url = from.url;
this.authentication = from.authentication;
}

public Map<String, String> getHeaders() {
var header = authentication.getHeader();
var responseMap = new HashMap<String, String>();
if (header != null) {
responseMap.put(header.getKey(), header.getValue());
}

return responseMap;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AasProvider that = (AasProvider) o;
return Objects.equals(url, that.url);
}

@Override
public int hashCode() {
return Objects.hashCode(url);
}

public URL getAccessUrl() {
return url.url();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige
* Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten
* Forschung e.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.fraunhofer.iosb.model.aas.auth;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import de.fraunhofer.iosb.model.aas.auth.impl.ApiKey;
import de.fraunhofer.iosb.model.aas.auth.impl.BasicAuth;
import de.fraunhofer.iosb.model.aas.auth.impl.NoAuth;
import org.jetbrains.annotations.Nullable;

import java.util.AbstractMap;
import java.util.Map;


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = BasicAuth.class, name = "basic"),
@JsonSubTypes.Type(value = ApiKey.class, name = "api-key"),
@JsonSubTypes.Type(value = NoAuth.class)
})
public abstract class AuthenticationMethod {

/**
* Get the header value to add to the request headers to communicate with the service.
* Headers: [... , (getHeader().key, getHeader().value), ...]
*
* @return The header to place in the request in order to authenticate
*/
public @Nullable Map.Entry<String, String> getHeader() {
return new AbstractMap.SimpleEntry<>("Authorization", getValue());
}

protected abstract String getValue();
}
Loading
Loading