diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/pom.xml b/basyx.aasenvironment/basyx.aasenvironment-client/pom.xml index 926097d98..4f3e5f019 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/pom.xml +++ b/basyx.aasenvironment/basyx.aasenvironment-client/pom.xml @@ -75,5 +75,33 @@ mockito-core test + + org.eclipse.digitaltwin.basyx + basyx.aasservice-client + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.aasservice-core + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.aasenvironment-feature-authorization + test + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/AuthorizedConnectedAasManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/AuthorizedConnectedAasManager.java new file mode 100644 index 000000000..a40cc652c --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/AuthorizedConnectedAasManager.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasenvironment.client; + +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.DescriptorResolverManager; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.authorization.AuthorizedAasDescriptorResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.authorization.AuthorizedSubmodelDescriptorResolver; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.main.client.AuthorizedConnectedAasRegistry; +import org.eclipse.digitaltwin.basyx.aasrepository.client.AuthorizedConnectedAasRepository; +import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.AuthorizedConnectedSubmodelRegistry; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.AuthorizedConnectedSubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; + +/** + * Authorized client component for executing consolidated Repository and Registry requests + * + * @author danish + * + */ +public class AuthorizedConnectedAasManager extends ConnectedAasManager { + + public AuthorizedConnectedAasManager(AuthorizedConnectedAasRegistry authorizedAasRegistryApi, AuthorizedConnectedAasRepository authorizedAasRepository, AuthorizedConnectedSubmodelRegistry authorizedSubmodelRegistryApi, AuthorizedConnectedSubmodelRepository authorizedSubmodelRepository) { + super(authorizedAasRegistryApi, authorizedAasRepository, authorizedSubmodelRegistryApi, authorizedSubmodelRepository, getAuthorizedResolver(authorizedAasRepository.getTokenManager(), authorizedSubmodelRepository.getTokenManager())); + } + + private static DescriptorResolverManager getAuthorizedResolver(TokenManager authorizedAasRepoTokenManager, TokenManager authorizedSubmodelRepoTokenManager) { + DescriptorResolver aasDescriptorResolver = new AuthorizedAasDescriptorResolver(new EndpointResolver(), authorizedAasRepoTokenManager); + DescriptorResolver smDescriptorResolver = new AuthorizedSubmodelDescriptorResolver(new EndpointResolver(), authorizedSubmodelRepoTokenManager); + + return new DescriptorResolverManager(aasDescriptorResolver, smDescriptorResolver); + } + +} \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java index bc913e7b4..a9ab86892 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManager.java @@ -38,11 +38,14 @@ import org.eclipse.digitaltwin.basyx.aasenvironment.client.exceptions.NoValidEndpointFoundException; import org.eclipse.digitaltwin.basyx.aasenvironment.client.exceptions.RegistryHttpRequestException; import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.AasDescriptorResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.DescriptorResolverManager; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.SubmodelDescriptorResolver; import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasrepository.client.ConnectedAasRepository; import org.eclipse.digitaltwin.basyx.aasrepository.feature.registry.integration.AasDescriptorFactory; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; @@ -53,7 +56,7 @@ /** * Client component for executing consolidated Repository and Registry requests * - * @author mateusmolina, jungjan + * @author mateusmolina, jungjan, danish * */ public class ConnectedAasManager { @@ -64,10 +67,10 @@ public class ConnectedAasManager { private final RegistryAndDiscoveryInterfaceApi aasRegistryApi; private final SubmodelRegistryApi smRegistryApi; - private final AasDescriptorResolver aasDescriptorResolver; + private final DescriptorResolver aasDescriptorResolver; private final AasDescriptorFactory aasDescriptorFactory; - private final SubmodelDescriptorResolver smDescriptorResolver; + private final DescriptorResolver smDescriptorResolver; private final SubmodelDescriptorFactory smDescriptorFactory; /** @@ -79,22 +82,21 @@ public class ConnectedAasManager { * @param submodelBaseRepositoryUrl */ public ConnectedAasManager(String aasRegistryBaseUrl, String aasRepositoryBaseUrl, String submodelRegistryBaseUrl, String submodelBaseRepositoryUrl) { - this(new RegistryAndDiscoveryInterfaceApi(aasRegistryBaseUrl), new ConnectedAasRepository(aasRepositoryBaseUrl), aasRepositoryBaseUrl, new SubmodelRegistryApi(submodelRegistryBaseUrl), - new ConnectedSubmodelRepository(submodelBaseRepositoryUrl), submodelBaseRepositoryUrl); + this(new RegistryAndDiscoveryInterfaceApi(aasRegistryBaseUrl), new ConnectedAasRepository(aasRepositoryBaseUrl), new SubmodelRegistryApi(submodelRegistryBaseUrl), + new ConnectedSubmodelRepository(submodelBaseRepositoryUrl), getResolver()); } - - ConnectedAasManager(RegistryAndDiscoveryInterfaceApi aasRegistryApi, ConnectedAasRepository aasRepository, String aasRepositoryBaseUrl, SubmodelRegistryApi smRegistryApi, ConnectedSubmodelRepository smRepository, - String submodelBaseRepositoryUrl) { + + ConnectedAasManager(RegistryAndDiscoveryInterfaceApi aasRegistryApi, ConnectedAasRepository aasRepository, SubmodelRegistryApi smRegistryApi, ConnectedSubmodelRepository smRepository, DescriptorResolverManager resolver) { this.aasRepository = aasRepository; this.aasRegistryApi = aasRegistryApi; this.smRepository = smRepository; this.smRegistryApi = smRegistryApi; - this.aasDescriptorResolver = ConnectedAasManagerHelper.buildAasDescriptorResolver(); - this.aasDescriptorFactory = ConnectedAasManagerHelper.buildAasDescriptorFactory(aasRepositoryBaseUrl); - this.smDescriptorResolver = ConnectedAasManagerHelper.buildSubmodelDescriptorResolver(); - this.smDescriptorFactory = ConnectedAasManagerHelper.buildSmDescriptorFactory(submodelBaseRepositoryUrl); + this.aasDescriptorResolver = resolver.getAasDescriptorResolver(); + this.aasDescriptorFactory = ConnectedAasManagerHelper.buildAasDescriptorFactory(aasRepository.getBaseUrl()); + this.smDescriptorResolver = resolver.getSubmodelDescriptorResolver(); + this.smDescriptorFactory = ConnectedAasManagerHelper.buildSmDescriptorFactory(smRepository.getBaseUrl()); } - + /** * Retrieves a ConnectedAasService in an AAS registry by its identifier. * @@ -102,7 +104,7 @@ public ConnectedAasManager(String aasRegistryBaseUrl, String aasRepositoryBaseUr * The identifier of the AAS to retrieve. * @return The retrieved ConnectedAasService object. */ - public ConnectedAasService getAas(String identifier) throws NoValidEndpointFoundException { + public ConnectedAasService getAasService(String identifier) throws NoValidEndpointFoundException { AssetAdministrationShellDescriptor descriptor; try { @@ -110,7 +112,7 @@ public ConnectedAasService getAas(String identifier) throws NoValidEndpointFound } catch (Exception e) { throw new RegistryHttpRequestException(identifier, e); } - return aasDescriptorResolver.resolveAasDescriptor(descriptor); + return aasDescriptorResolver.resolveDescriptor(descriptor); } /** @@ -121,7 +123,7 @@ public ConnectedAasService getAas(String identifier) throws NoValidEndpointFound * The identifier of the submodel to retrieve. * @return The retrieved ConnectedSubmodelService object. */ - public ConnectedSubmodelService getSubmodel(String identifier) { + public ConnectedSubmodelService getSubmodelService(String identifier) { SubmodelDescriptor descriptor; try { @@ -130,7 +132,7 @@ public ConnectedSubmodelService getSubmodel(String identifier) { throw new RegistryHttpRequestException(identifier, e); } - return smDescriptorResolver.resolveSubmodelDescriptor(descriptor); + return smDescriptorResolver.resolveDescriptor(descriptor); } /** @@ -141,11 +143,11 @@ public ConnectedSubmodelService getSubmodel(String identifier) { * @return The retrieved Submodel object. */ public List getAllSubmodels(String shellIdentifier) { - AssetAdministrationShell shell = getAas(shellIdentifier).getAAS(); + AssetAdministrationShell shell = getAasService(shellIdentifier).getAAS(); List submodelReferences = shell.getSubmodels(); return submodelReferences.parallelStream() .map(this::extractSubmodelIdentifierFromReference) - .map(this::getSubmodel) + .map(this::getSubmodelService) .collect(Collectors.toList()); } @@ -247,5 +249,12 @@ private Key extractSubmodelKeyFromReference(Reference submodelReference) { return submodelReference.getKeys() .get(0); } + + private static DescriptorResolverManager getResolver() { + DescriptorResolver aasDescriptorResolver = new AasDescriptorResolver(new EndpointResolver()); + DescriptorResolver smDescriptorResolver = new SubmodelDescriptorResolver(new EndpointResolver()); + + return new DescriptorResolverManager(aasDescriptorResolver, smDescriptorResolver); + } } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java index 6505c8f3e..ab6044d7f 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/ConnectedAasManagerHelper.java @@ -26,9 +26,6 @@ package org.eclipse.digitaltwin.basyx.aasenvironment.client; -import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.AasDescriptorResolver; -import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; -import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.SubmodelDescriptorResolver; import org.eclipse.digitaltwin.basyx.aasrepository.feature.registry.integration.AasDescriptorFactory; import org.eclipse.digitaltwin.basyx.aasrepository.feature.registry.integration.mapper.AttributeMapper; import org.eclipse.digitaltwin.basyx.http.Aas4JHTTPSerializationExtension; @@ -41,7 +38,7 @@ /** * Provides builder methods for {@link ConnectedAasManager} dependencies * - * @author mateusmolina + * @author mateusmolina, danish * */ class ConnectedAasManagerHelper { @@ -58,14 +55,6 @@ static ObjectMapper buildObjectMapper() { return builder.build(); } - static AasDescriptorResolver buildAasDescriptorResolver() { - return new AasDescriptorResolver(new EndpointResolver()); - } - - static SubmodelDescriptorResolver buildSubmodelDescriptorResolver() { - return new SubmodelDescriptorResolver(new EndpointResolver()); - } - static AasDescriptorFactory buildAasDescriptorFactory(String aasRepositoryBaseUrl) { AttributeMapper attributeMapper = new AttributeMapper(objectMapper); diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java index 927586b6c..6e743a8e1 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/AasDescriptorResolver.java @@ -31,14 +31,15 @@ import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.Endpoint; import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; /** - * Resolves an AasDescriptor into an AssetAdministrationShell + * Resolves an AasDescriptor into a {@link ConnectedAasService} * - * @author mateusmolina + * @author mateusmolina, danish * */ -public class AasDescriptorResolver { +public class AasDescriptorResolver implements DescriptorResolver { private final EndpointResolver endpointResolver; @@ -50,20 +51,20 @@ public class AasDescriptorResolver { public AasDescriptorResolver(EndpointResolver endpointResolver) { this.endpointResolver = endpointResolver; } - + /** * Resolves an AASDescriptor to a ConnectedAasService * * @param aasDescriptor * @return */ - public ConnectedAasService resolveAasDescriptor(AssetAdministrationShellDescriptor aasDescriptor) { + public ConnectedAasService resolveDescriptor(AssetAdministrationShellDescriptor aasDescriptor) { String endpoint = endpointResolver.resolveFirst(aasDescriptor.getEndpoints(), AasDescriptorResolver::parseEndpoint); return new ConnectedAasService(endpoint); } - private static Optional parseEndpoint(Endpoint endpoint) { + public static Optional parseEndpoint(Endpoint endpoint) { try { if (endpoint == null || endpoint.getProtocolInformation() == null || endpoint.getProtocolInformation() .getHref() == null) diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/DescriptorResolverManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/DescriptorResolverManager.java new file mode 100644 index 000000000..d3e807b9b --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/DescriptorResolverManager.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers; + +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; + +/** + * A helper class for accumulating various {@link DescriptorResolver}s + * + * @author danish + */ +public class DescriptorResolverManager { + + private DescriptorResolver aasDescriptorResolver; + private DescriptorResolver submodelDescriptorResolver; + + public DescriptorResolverManager(DescriptorResolver aasDescriptorResolver, DescriptorResolver submodelDescriptorResolver) { + super(); + this.aasDescriptorResolver = aasDescriptorResolver; + this.submodelDescriptorResolver = submodelDescriptorResolver; + } + + public DescriptorResolver getAasDescriptorResolver() { + return aasDescriptorResolver; + } + + public DescriptorResolver getSubmodelDescriptorResolver() { + return submodelDescriptorResolver; + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/EndpointResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/EndpointResolver.java index 62675d9f1..75a7d85df 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/EndpointResolver.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/EndpointResolver.java @@ -106,7 +106,7 @@ private boolean isURIWorking(URI uri) { connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); int responseCode = connection.getResponseCode(); - return (200 <= responseCode && responseCode <= 399); + return (200 <= responseCode && responseCode <= 401); } catch (Exception e) { return false; } finally { diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java index dc0cb6c4e..5ddc10d72 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/SubmodelDescriptorResolver.java @@ -25,6 +25,7 @@ package org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; import java.net.URI; import java.util.Optional; @@ -33,12 +34,12 @@ import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; /** - * Resolves a SubmodelDescriptor into a Submodel + * Resolves a SubmodelDescriptor into a {@link ConnectedSubmodelService} * - * @author mateusmolina + * @author mateusmolina, danish * */ -public class SubmodelDescriptorResolver { +public class SubmodelDescriptorResolver implements DescriptorResolver { private final EndpointResolver endpointResolver; @@ -58,13 +59,13 @@ public SubmodelDescriptorResolver(EndpointResolver endpointResolver) { * the Submodel Descriptor to be resolved * @return the Connected submodelserver */ - public ConnectedSubmodelService resolveSubmodelDescriptor(SubmodelDescriptor smDescriptor) { + public ConnectedSubmodelService resolveDescriptor(SubmodelDescriptor smDescriptor) { String endpoint = endpointResolver.resolveFirst(smDescriptor.getEndpoints(), SubmodelDescriptorResolver::parseEndpoint); return new ConnectedSubmodelService(endpoint); } - private static Optional parseEndpoint(Endpoint endpoint) { + public static Optional parseEndpoint(Endpoint endpoint) { try { if (endpoint == null || endpoint.getProtocolInformation() == null || endpoint.getProtocolInformation() .getHref() == null) diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/URIParser.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/URIParser.java index c9f56434f..a8fccfa72 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/URIParser.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/URIParser.java @@ -35,6 +35,6 @@ * */ @FunctionalInterface -interface URIParser { +public interface URIParser { public Optional parse(T object); } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedAasDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedAasDescriptorResolver.java new file mode 100644 index 000000000..840dbc36b --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedAasDescriptorResolver.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.authorization; + +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.AasDescriptorResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasservice.client.AuthorizedConnectedAasService; +import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; + +/** + * Resolves an AasDescriptor into a {@link AuthorizedConnectedAasService} + * + * @author danish + * + */ +public class AuthorizedAasDescriptorResolver implements DescriptorResolver { + + private final EndpointResolver endpointResolver; + private final TokenManager tokenManager; + + public AuthorizedAasDescriptorResolver(EndpointResolver endpointResolver, TokenManager tokenManager) { + this.endpointResolver = endpointResolver; + this.tokenManager = tokenManager; + } + + @Override + public AuthorizedConnectedAasService resolveDescriptor(AssetAdministrationShellDescriptor descriptor) { + String endpoint = endpointResolver.resolveFirst(descriptor.getEndpoints(), AasDescriptorResolver::parseEndpoint); + + return new AuthorizedConnectedAasService(endpoint, tokenManager); + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedSubmodelDescriptorResolver.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedSubmodelDescriptorResolver.java new file mode 100644 index 000000000..350e52c35 --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/main/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/authorization/AuthorizedSubmodelDescriptorResolver.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.authorization; + +import org.eclipse.digitaltwin.aas4j.v3.model.Key; +import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.Reference; +import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.SubmodelDescriptorResolver; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelservice.client.AuthorizedConnectedSubmodelService; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; + +/** + * Resolves an SubmodelDescriptor into a {@link AuthorizedConnectedSubmodelService} + * + * @author danish + * + */ +public class AuthorizedSubmodelDescriptorResolver implements DescriptorResolver { + + private final EndpointResolver endpointResolver; + private final TokenManager tokenManager; + + public AuthorizedSubmodelDescriptorResolver(EndpointResolver endpointResolver, TokenManager tokenManager) { + this.endpointResolver = endpointResolver; + this.tokenManager = tokenManager; + } + + @Override + public AuthorizedConnectedSubmodelService resolveDescriptor(SubmodelDescriptor descriptor) { + String endpoint = endpointResolver.resolveFirst(descriptor.getEndpoints(), SubmodelDescriptorResolver::parseEndpoint); + + return new AuthorizedConnectedSubmodelService(endpoint, tokenManager); + } + + public Reference deriveReferenceFromSubmodelDescriptor(SubmodelDescriptor smDescriptor) { + return new DefaultReference.Builder().type(ReferenceTypes.EXTERNAL_REFERENCE).keys(generateKeyFromId(smDescriptor.getId())).build(); + } + + private static Key generateKeyFromId(String smId) { + return new DefaultKey.Builder().type(KeyTypes.SUBMODEL).value(smId).build(); + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestAuthorizedConnectedAasManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestAuthorizedConnectedAasManager.java new file mode 100644 index 000000000..98ca90893 --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestAuthorizedConnectedAasManager.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasenvironment.client; + +import static org.mockito.Mockito.spy; +import java.io.IOException; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.aasregistry.client.ApiException; +import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.main.client.AuthorizedConnectedAasRegistry; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.client.AuthorizedConnectedAasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.client.ConnectedAasRepository; +import org.eclipse.digitaltwin.basyx.aasservice.client.TestAuthorizedConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.AuthorizedConnectedSubmodelRegistry; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.AuthorizedConnectedSubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.ConnectedSubmodelRepository; +import org.junit.BeforeClass; +import org.springframework.boot.SpringApplication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Test for {@link AuthorizedConnectedAasManager} + * + * @author danish + * + */ +public class TestAuthorizedConnectedAasManager extends TestConnectedAasManager { + + private static final String PROFILE = "authorization"; + protected final static String AAS_REGISTRY_BASE_PATH = "http://localhost:8051"; + protected final static String SM_REGISTRY_BASE_PATH = "http://localhost:8061"; + + private final static TokenManager TOKEN_MANAGER = new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom"))); + private final static TokenManager TOKEN_MANAGER_REGISTRY = new TokenManager("http://localhost:9097/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom"))); + + private static AuthorizedConnectedAasRepository connectedAasRepository; + private static AuthorizedConnectedSubmodelRepository connectedSmRepository; + private static AuthorizedConnectedAasRegistry aasRegistryApi; + private static AuthorizedConnectedSubmodelRegistry smRegistryApi; + + private AuthorizedConnectedAasManager aasManager; + + @BeforeClass + public static void initApplication() { + SpringApplication application = new SpringApplication(DummyAasEnvironmentComponent.class); + application.setAdditionalProfiles(PROFILE); + + appContext = application.run(new String[] {}); + + aasRepository = appContext.getBean(AasRepository.class); + smRepository = appContext.getBean(SubmodelRepository.class); + } + + @Override + protected ConnectedAasManager getConnectedAasManager() { + + aasManager = new AuthorizedConnectedAasManager(aasRegistryApi, connectedAasRepository, smRegistryApi, connectedSmRepository); + + return aasManager; + } + + @Override + protected SubmodelRegistryApi getConnectedSubmodelRegistry() { + + smRegistryApi = spy(new AuthorizedConnectedSubmodelRegistry(SM_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY)); + + return smRegistryApi; + } + + @Override + protected RegistryAndDiscoveryInterfaceApi getConnectedAasRegistry() { + + aasRegistryApi = spy(new AuthorizedConnectedAasRegistry(AAS_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY)); + + return aasRegistryApi; + } + + @Override + protected ConnectedSubmodelRepository getConnectedSubmodelRepo() { + + connectedSmRepository = spy(new AuthorizedConnectedSubmodelRepository(SM_REPOSITORY_BASE_PATH, TOKEN_MANAGER)); + + return connectedSmRepository; + } + + @Override + protected ConnectedAasRepository getConnectedAasRepo() { + + connectedAasRepository = spy(new AuthorizedConnectedAasRepository(AAS_REPOSITORY_BASE_PATH, TOKEN_MANAGER)); + + return connectedAasRepository; + } + + @Override + protected Submodel getSubmodelFromManager(String submodelId) { + return aasManager.getSubmodelService(submodelId).getSubmodel(); + } + + @Override + protected AssetAdministrationShellDescriptor getDescriptorFromAasRegistry(String shellId) throws ApiException { + return new AuthorizedConnectedAasRegistry(AAS_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).getAssetAdministrationShellDescriptorById(shellId); + } + + @Override + protected AssetAdministrationShell getAasFromRepo(String shellId) { + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + } catch (IOException e) { + e.printStackTrace(); + } + + AssetAdministrationShell assetAdministrationShell = aasRepository.getAas(shellId); + + SecurityContextHolder.clearContext(); + + return assetAdministrationShell; + } + + @Override + protected SubmodelDescriptor getDescriptorFromSubmodelRegistry(String submodelId) throws org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException { + return new AuthorizedConnectedSubmodelRegistry(SM_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).getSubmodelDescriptorById(submodelId); + } + + @Override + protected Submodel getSubmodelFromRepo(String submodelId) { + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + } catch (IOException e) { + e.printStackTrace(); + } + + Submodel submodel = smRepository.getSubmodel(submodelId); + + SecurityContextHolder.clearContext(); + + return submodel; + } + + @Override + protected AssetAdministrationShell getAasFromManager(String shellId) { + return aasManager.getAasService(shellId).getAAS(); + } + + @Override + protected void populateRegistries() { + try { + new AuthorizedConnectedAasRegistry(AAS_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).postAssetAdministrationShellDescriptor(FIXTURE.buildAasPre1Descriptor()); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + new AuthorizedConnectedSubmodelRegistry(SM_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).postSubmodelDescriptor(FIXTURE.buildSmPre1Descriptor()); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + @Override + protected void cleanUpRegistries() { + try { + new AuthorizedConnectedAasRegistry(AAS_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).deleteAllShellDescriptors(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + new AuthorizedConnectedSubmodelRegistry(SM_REGISTRY_BASE_PATH, TOKEN_MANAGER_REGISTRY).deleteAllSubmodelDescriptors(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + @Override + protected void populateRepositories() { + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + aasRepository.createAas(FIXTURE.buildAasPre1()); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + smRepository.createSubmodel(FIXTURE.buildSmPre1()); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + @Override + protected void cleanUpRepositories() { + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + aasRepository.deleteAas(TestFixture.AAS_PRE1_ID); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + aasRepository.deleteAas(TestFixture.AAS_POS1_ID); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + smRepository.deleteSubmodel(TestFixture.SM_PRE1_ID); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + try { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + smRepository.deleteSubmodel(TestFixture.SM_POS1_ID); + + SecurityContextHolder.clearContext(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + +} diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManager.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManager.java index 8ba010436..5263c68e6 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManager.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/TestConnectedAasManager.java @@ -40,11 +40,17 @@ import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; import org.eclipse.digitaltwin.basyx.aasenvironment.client.exceptions.NoValidEndpointFoundException; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.AasDescriptorResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.DescriptorResolverManager; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.EndpointResolver; +import org.eclipse.digitaltwin.basyx.aasenvironment.client.resolvers.SubmodelDescriptorResolver; import org.eclipse.digitaltwin.basyx.aasregistry.client.ApiException; import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; import org.eclipse.digitaltwin.basyx.aasrepository.client.ConnectedAasRepository; +import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.resolver.DescriptorResolver; import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; @@ -67,23 +73,23 @@ * */ public class TestConnectedAasManager { - private final static String AAS_REPOSITORY_BASE_PATH = "http://localhost:8081"; - private final static String SM_REPOSITORY_BASE_PATH = "http://localhost:8081"; - private final static String AAS_REGISTRY_BASE_PATH = "http://localhost:8050"; - private final static String SM_REGISTRY_BASE_PATH = "http://localhost:8060"; + protected final static String AAS_REPOSITORY_BASE_PATH = "http://localhost:8081"; + protected final static String SM_REPOSITORY_BASE_PATH = "http://localhost:8081"; + protected final static String AAS_REGISTRY_BASE_PATH = "http://localhost:8050"; + protected final static String SM_REGISTRY_BASE_PATH = "http://localhost:8060"; - private static ConfigurableApplicationContext appContext; - private static AasRepository aasRepository; - private static SubmodelRepository smRepository; + protected static ConfigurableApplicationContext appContext; + protected static AasRepository aasRepository; + protected static SubmodelRepository smRepository; - private final static TestFixture FIXTURE = new TestFixture(AAS_REPOSITORY_BASE_PATH, SM_REPOSITORY_BASE_PATH); + protected final static TestFixture FIXTURE = new TestFixture(AAS_REPOSITORY_BASE_PATH, SM_REPOSITORY_BASE_PATH); - private static ConnectedAasRepository connectedAasRepository; - private static ConnectedSubmodelRepository connectedSmRepository; - private static RegistryAndDiscoveryInterfaceApi aasRegistryApi; - private static SubmodelRegistryApi smRegistryApi; + protected static ConnectedAasRepository connectedAasRepository; + protected static ConnectedSubmodelRepository connectedSmRepository; + protected static RegistryAndDiscoveryInterfaceApi aasRegistryApi; + protected static SubmodelRegistryApi smRegistryApi; - private ConnectedAasManager aasManager; + protected ConnectedAasManager aasManager; @BeforeClass public static void initApplication() { @@ -100,12 +106,12 @@ public static void cleanUpContext() { @Before public void setupRepositories() { - connectedAasRepository = spy(new ConnectedAasRepository(AAS_REPOSITORY_BASE_PATH)); - connectedSmRepository = spy(new ConnectedSubmodelRepository(SM_REPOSITORY_BASE_PATH)); - aasRegistryApi = spy(new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH)); - smRegistryApi = spy(new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH)); - - aasManager = new ConnectedAasManager(aasRegistryApi, connectedAasRepository, AAS_REPOSITORY_BASE_PATH, smRegistryApi, connectedSmRepository, SM_REPOSITORY_BASE_PATH); + connectedAasRepository = getConnectedAasRepo(); + connectedSmRepository = getConnectedSubmodelRepo(); + aasRegistryApi = getConnectedAasRegistry(); + smRegistryApi = getConnectedSubmodelRegistry(); + + aasManager = getConnectedAasManager(); cleanUpRegistries(); populateRepositories(); @@ -132,8 +138,8 @@ public void createAas() throws ApiException { inOrder.verify(aasRegistryApi, times(1)) .postAssetAdministrationShellDescriptor(expectedDescriptor); - assertEquals(expectedAas, aasRepository.getAas(TestFixture.AAS_POS1_ID)); - assertEquals(expectedDescriptor, new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH).getAssetAdministrationShellDescriptorById(TestFixture.AAS_POS1_ID)); + assertEquals(expectedAas, getAasFromRepo(TestFixture.AAS_POS1_ID)); + assertEquals(expectedDescriptor, getDescriptorFromAasRegistry(TestFixture.AAS_POS1_ID)); } @Test @@ -152,8 +158,8 @@ public void createSubmodelInAas() throws Exception { inOrder.verify(connectedAasRepository, times(1)) .addSubmodelReference(eq(TestFixture.AAS_PRE1_ID), any()); - assertEquals(expectedSm, smRepository.getSubmodel(TestFixture.SM_POS1_ID)); - assertEquals(expectedDescriptor, new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH).getSubmodelDescriptorById(TestFixture.SM_POS1_ID)); + assertEquals(expectedSm, getSubmodelFromRepo(TestFixture.SM_POS1_ID)); + assertEquals(expectedDescriptor, getDescriptorFromSubmodelRegistry(TestFixture.SM_POS1_ID)); } @Test @@ -167,8 +173,8 @@ public void deleteAas() throws ApiException { inOrder.verify(connectedAasRepository, times(1)) .deleteAas(TestFixture.AAS_PRE1_ID); - assertThrows(ElementDoesNotExistException.class, () -> aasRepository.getAas(TestFixture.AAS_PRE1_ID)); - assertThrows(Exception.class, () -> new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH).getAssetAdministrationShellDescriptorById(TestFixture.AAS_PRE1_ID)); + assertThrows(ElementDoesNotExistException.class, () -> getAasFromRepo(TestFixture.AAS_PRE1_ID)); + assertThrows(Exception.class, () -> getDescriptorFromAasRegistry(TestFixture.AAS_PRE1_ID)); } @Test @@ -184,15 +190,15 @@ public void deleteSubmodelOfAas() throws Exception { inOrder.verify(connectedSmRepository, times(1)) .deleteSubmodel(TestFixture.SM_PRE1_ID); - assertThrows(ElementDoesNotExistException.class, () -> smRepository.getSubmodel(TestFixture.SM_PRE1_ID)); - assertThrows(Exception.class, () -> new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH).getSubmodelDescriptorById(TestFixture.SM_PRE1_ID)); + assertThrows(ElementDoesNotExistException.class, () -> getSubmodelFromRepo(TestFixture.SM_PRE1_ID)); + assertThrows(Exception.class, () -> getDescriptorFromSubmodelRegistry(TestFixture.SM_PRE1_ID)); } @Test public void getAas() throws ApiException, NoValidEndpointFoundException { AssetAdministrationShell expectedAas = FIXTURE.buildAasPre1(); - AssetAdministrationShell actualAas = aasManager.getAas(TestFixture.AAS_PRE1_ID) + AssetAdministrationShell actualAas = aasManager.getAasService(TestFixture.AAS_PRE1_ID) .getAAS(); assertEquals(expectedAas, actualAas); @@ -201,13 +207,62 @@ public void getAas() throws ApiException, NoValidEndpointFoundException { @Test public void getSubmodel() throws Exception { Submodel expectedSm = FIXTURE.buildSmPre1(); - - Submodel actualSm = aasManager.getSubmodel(TestFixture.SM_PRE1_ID) + + Submodel actualSm = aasManager.getSubmodelService(TestFixture.SM_PRE1_ID) .getSubmodel(); assertEquals(expectedSm, actualSm); } + protected Submodel getSubmodelFromManager(String submodelId) { + return aasManager.getSubmodelService(submodelId).getSubmodel(); + } + + protected AssetAdministrationShellDescriptor getDescriptorFromAasRegistry(String shellId) throws ApiException { + return new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH).getAssetAdministrationShellDescriptorById(shellId); + } + + protected AssetAdministrationShell getAasFromRepo(String shellId) { + return aasRepository.getAas(shellId); + } + + protected SubmodelDescriptor getDescriptorFromSubmodelRegistry(String submodelId) throws org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException { + return new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH).getSubmodelDescriptorById(submodelId); + } + + protected Submodel getSubmodelFromRepo(String submodelId) { + return smRepository.getSubmodel(TestFixture.SM_POS1_ID); + } + + protected AssetAdministrationShell getAasFromManager(String shellId) { + return aasManager.getAasService(shellId).getAAS(); + } + + protected ConnectedAasManager getConnectedAasManager() { + DescriptorResolver aasDescriptorResolver = new AasDescriptorResolver(new EndpointResolver()); + DescriptorResolver smDescriptorResolver = new SubmodelDescriptorResolver(new EndpointResolver()); + + DescriptorResolverManager descriptorResolverManager = new DescriptorResolverManager(aasDescriptorResolver, smDescriptorResolver); + + return new ConnectedAasManager(aasRegistryApi, connectedAasRepository, smRegistryApi, connectedSmRepository, descriptorResolverManager); + } + + protected SubmodelRegistryApi getConnectedSubmodelRegistry() { + return spy(new SubmodelRegistryApi(SM_REGISTRY_BASE_PATH)); + } + + protected RegistryAndDiscoveryInterfaceApi getConnectedAasRegistry() { + return spy(new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH)); + } + + protected ConnectedSubmodelRepository getConnectedSubmodelRepo() { + return spy(new ConnectedSubmodelRepository(SM_REPOSITORY_BASE_PATH)); + } + + protected ConnectedAasRepository getConnectedAasRepo() { + return spy(new ConnectedAasRepository(AAS_REPOSITORY_BASE_PATH)); + } + @Test public void getAllSubmodels() { Submodel otherExpectedSubmodel = FIXTURE.buildSmPos1(); @@ -223,7 +278,7 @@ public void getAllSubmodels() { } - private void populateRegistries() { + protected void populateRegistries() { try { new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH).postAssetAdministrationShellDescriptor(FIXTURE.buildAasPre1Descriptor()); } catch (Exception e) { @@ -236,7 +291,7 @@ private void populateRegistries() { } } - private void cleanUpRegistries() { + protected void cleanUpRegistries() { try { new RegistryAndDiscoveryInterfaceApi(AAS_REGISTRY_BASE_PATH).deleteAllShellDescriptors(); } catch (Exception e) { @@ -249,7 +304,7 @@ private void cleanUpRegistries() { } } - private void populateRepositories() { + protected void populateRepositories() { try { aasRepository.createAas(FIXTURE.buildAasPre1()); } catch (Exception e) { @@ -262,7 +317,7 @@ private void populateRepositories() { } } - private void cleanUpRepositories() { + protected void cleanUpRepositories() { try { aasRepository.deleteAas(TestFixture.AAS_PRE1_ID); } catch (Exception e) { diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java index 533119693..e0666db0d 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/java/org/eclipse/digitaltwin/basyx/aasenvironment/client/resolvers/RegistryDescriptorResolverTest.java @@ -77,7 +77,8 @@ public void resolveAasDescriptor() throws ApiException { AasDescriptorResolver resolver = new AasDescriptorResolver(new EndpointResolver()); AssetAdministrationShell expectedAas = FIXTURE.buildAasPre1(); - AssetAdministrationShell actualAas = resolver.resolveAasDescriptor(FIXTURE.buildAasPre1Descriptor()).getAAS(); + + AssetAdministrationShell actualAas = resolver.resolveDescriptor(FIXTURE.buildAasPre1Descriptor()).getAAS(); assertEquals(expectedAas, actualAas); } @@ -87,7 +88,8 @@ public void resolveSmDescriptor() throws ApiException { SubmodelDescriptorResolver resolver = new SubmodelDescriptorResolver(new EndpointResolver()); Submodel expectedSm = FIXTURE.buildSmPre1(); - Submodel actualSm = resolver.resolveSubmodelDescriptor(FIXTURE.buildSmPre1Descriptor()).getSubmodel(); + + Submodel actualSm = resolver.resolveDescriptor(FIXTURE.buildSmPre1Descriptor()).getSubmodel(); assertEquals(expectedSm, actualSm); } diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/application-authorization.properties b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/application-authorization.properties new file mode 100644 index 000000000..ce03007f1 --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/application-authorization.properties @@ -0,0 +1,61 @@ +server.port=8081 +spring.application.name=AAS Environment + +basyx.backend = InMemory + +#basyx.backend = MongoDB +#spring.data.mongodb.host=mongo +# or spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=aasenvironments +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + +# basyx.aasrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 +# basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD + +#################################################################################### +# Preconfiguring the Environment; +#################################################################################### +# Comma seperated list that contains Environment files to load on startup +# To load from Classpath (src/main/resources) use classpath:path/to/file.end +# To load from Filesystem ( On your local machine ) use the prefix file: +# +# basyx.environment = classpath:testEnvironment.json,classpath:testEnvironment.xml,file:C:\\Users\\Administrator\\Documents\\01_Festo.aasx,file:/var/www/html/01_Submodel.json +# + +#################################################################################### +# Authorization +#################################################################################### +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = rbac +basyx.feature.authorization.jwtBearerTokenProvider = keycloak +basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx + +#################################################################################### +# Operation Delegation +#################################################################################### +# This feature is enabled by default + +#basyx.submodelrepository.feature.operation.delegation.enabled = false + +#################################################################################### +# Max File Size +#################################################################################### +# To define the maximum size of file to be uploaded in a request (default 1 MB) + +# spring.servlet.multipart.max-file-size=128KB + +#################################################################################### +# Max Request Size +#################################################################################### +# To define the total request size for a multipart/form-data (default 10 MB) + +# spring.servlet.multipart.max-request-size=128KB \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/authorization/modulus.txt b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/authorization/modulus.txt @@ -0,0 +1 @@ +sQ4RUvlZXLjQpLizIMht46ASwpwUoPpUDTmUhxF_VV-ezHTHbTAdv_5RA1GgCyTjAQe_Ih6dLByZrJFaroyvqgIJdMRCb0MwajI1US0_NwHtVvo5dea_-GKeHGRzvYZjxVlooR_1xmskfAM_NR_NaOMUhr_TNV7n7LXEb06L55DYnqdqrUnhXLewBq1lo54GsMqxN4hlkc4nJ2uYUtWEkV4SlMyYRjXBlylpuWFO0-_FsmaqSx7CZWjNWmqKlxnvUgRrT5Vh-9ZgCAOmGtLuFYOVWzqmVjWtyiJ1pYfWjwc86XeWBFefVY1lkoNNoSYKV4AZIkeF2M_-FHNzNGhLOw \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/rbac_rules.json b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..8a91a29f8 --- /dev/null +++ b/basyx.aasenvironment/basyx.aasenvironment-client/src/test/resources/rbac_rules.json @@ -0,0 +1,656 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, + { + "role": "basyx-asset-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-asset-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + }, + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE", "EXECUTE"], + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificSubmodelElementIdShort" + } + }, + { + "role": "basyx-sme-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-sme-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2" + } + }, + { + "role": "basyx-sme-updater-three", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc1.specificSubmodelElementIdShort-2" + } + }, + { + "role": "basyx-file-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor-two", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "square" + } + }, + { + "role": "basyx-file-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "specificConceptDescriptionId" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "specificConceptDescriptionId" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "specificConceptDescriptionId-2" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-reader-serialization", + "action": "READ", + "targetInformation": { + "@type": "aas-environment", + "aasIds": ["shell001", "shell002"], + "submodelIds": ["7A7104BDAB57E184", "AC69B1CB44F07935"] + } + }, + { + "role": "basyx-reader-serialization", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-reader-serialization", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-serialization", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-reader-serialization-two", + "action": "READ", + "targetInformation": { + "@type": "aas-environment", + "aasIds": ["shell001", "shell002"], + "submodelIds": ["7A7104BDAB57E184"] + } + }, + { + "role": "basyx-uploader", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "UPDATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader", + "action": "CREATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader", + "action": "CREATE", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "*", + "submodelIds": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168" + } + }, + { + "role": "basyx-uploader-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168" + } + }, + { + "role": "basyx-uploader-two", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168" + } + }, + { + "role": "basyx-uploader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "UPDATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "CREATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-two", + "action": "CREATE", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168", + "submodelIds": ["http://i40.customer.com/type/1/1/7A7104BDAB57E184", "http://i40.customer.com/type/1/1/1A7B62B529F19152", "http://i40.customer.com/instance/1/1/AC69B1CB44F07935"] + } + }, + { + "role": "basyx-uploader-three", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168" + } + }, + { + "role": "basyx-uploader-three", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "http://customer.com/aas/9175_7013_7091_9168" + } + }, + { + "role": "basyx-uploader-three", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "READ", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "UPDATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "CREATE", + "targetInformation": { + "@type": "concept-description", + "conceptDescriptionIds": "*" + } + }, + { + "role": "basyx-uploader-three", + "action": "CREATE", + "targetInformation": { + "@type": "aas-environment", + "aasIds": "AuthorizedAasID", + "submodelIds": ["http://i40.customer.com/type/1/1/7A7104BDAB57E184", "http://i40.customer.com/type/1/1/1A7B62B529F19152", "http://i40.customer.com/instance/1/1/AC69B1CB44F07935"] + } + } +] \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/pom.xml b/basyx.aasregistry/basyx.aasregistry-client-native/pom.xml index da0cc8bc0..eb589f472 100644 --- a/basyx.aasregistry/basyx.aasregistry-client-native/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-client-native/pom.xml @@ -19,7 +19,6 @@ ${openapi.result.folder}/${openapi.name} - src/generated/java maven-clean-plugin @@ -52,8 +51,10 @@ jsonpatch-maven-plugin - ${project.basedir}/../${openapi.folder.name}/${openapi.base.name} - ${project.basedir}/../${openapi.folder.name}/${patch.base-extensions.name} + + ${project.basedir}/../${openapi.folder.name}/${openapi.base.name} + + ${project.basedir}/../${openapi.folder.name}/${patch.base-extensions.name} ${openapi.result.file} @@ -72,9 +73,12 @@ native ${openapi.result.file} ${project.basedir}/${generated.folder} - org.eclipse.digitaltwin.basyx.aasregistry.client.api - org.eclipse.digitaltwin.basyx.aasregistry.client - org.eclipse.digitaltwin.basyx.aasregistry.client.model + + org.eclipse.digitaltwin.basyx.aasregistry.client.api + + org.eclipse.digitaltwin.basyx.aasregistry.client + + org.eclipse.digitaltwin.basyx.aasregistry.client.model true true false @@ -133,6 +137,10 @@ com.google.code.findbugs jsr305 + + org.eclipse.digitaltwin.basyx + basyx.client + diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/AuthorizedConnectedAasRegistry.java b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/AuthorizedConnectedAasRegistry.java new file mode 100644 index 000000000..5b340f36a --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/main/client/AuthorizedConnectedAasRegistry.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.main.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; + +/** + * Provides access to an authorized Aas Registry on a remote server + * + * @author danish + */ +public class AuthorizedConnectedAasRegistry extends RegistryAndDiscoveryInterfaceApi { + + private final TokenManager tokenManager; + private final String aasRegistryBasePath; + + /** + * + * @param basePath + * the Url of the Aas Registry without the "/shell-descriptors" part + * @param tokenManager + */ + public AuthorizedConnectedAasRegistry(String basePath, TokenManager tokenManager) { + super(basePath, getRequestBuilder(tokenManager)); + this.aasRegistryBasePath = basePath; + this.tokenManager = tokenManager; + } + + public String getBaseUrl() { + return aasRegistryBasePath; + } + + public TokenManager getTokenManager() { + return tokenManager; + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder().header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-client-native/templates/libraries/native/api.mustache b/basyx.aasregistry/basyx.aasregistry-client-native/templates/libraries/native/api.mustache index 5ec50e316..d5d40059e 100644 --- a/basyx.aasregistry/basyx.aasregistry-client-native/templates/libraries/native/api.mustache +++ b/basyx.aasregistry/basyx.aasregistry-client-native/templates/libraries/native/api.mustache @@ -59,17 +59,36 @@ public class {{classname}} { private final Duration memberVarReadTimeout; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarResponseInterceptor; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarAsyncResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public {{classname}}() { this(new ApiClient()); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(HttpRequest.Builder httpRequestBuilder) { + this(new ApiClient()); + this.httpRequestBuilder = httpRequestBuilder; } public {{classname}}(String protocol, String host, int port) { this(protocol + "://" + host + ":" + port); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(String protocol, String host, int port, HttpRequest.Builder httpRequestBuilder) { + this(protocol + "://" + host + ":" + port); + this.httpRequestBuilder = httpRequestBuilder; } public {{classname}}(String basePath) { this(withBaseUri(new ApiClient(), basePath)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(String basePath, HttpRequest.Builder httpRequestBuilder) { + this(withBaseUri(new ApiClient(), basePath)); + this.httpRequestBuilder = httpRequestBuilder; } private static ApiClient withBaseUri(ApiClient client, String uri) { @@ -361,7 +380,7 @@ public class {{classname}} { {{#hasPathParams}}{{#pathParams}}{{#vendorExtensions.x-utf8-base64-url-encoded-as-string}}String {{{paramName}}}AsBase64EncodedParam = {{{paramName}}} == null ? null : new String(java.util.Base64.getUrlEncoder().encode({{paramName}}.getBytes(java.nio.charset.StandardCharsets.UTF_8)), java.nio.charset.StandardCharsets.UTF_8); {{/vendorExtensions.x-utf8-base64-url-encoded-as-string}}{{/pathParams}}{{/hasPathParams}} - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); {{! Switch delimiters for baseName so we can write constants like "{query}" }} String localVarPath = "{{{path}}}"{{#pathParams}} diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java new file mode 100644 index 000000000..0fb2192e1 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedClientTest.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.service.storage.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import org.eclipse.digitaltwin.basyx.aasregistry.client.ApiException; +import org.eclipse.digitaltwin.basyx.aasregistry.client.api.RegistryAndDiscoveryInterfaceApi; +import org.eclipse.digitaltwin.basyx.aasregistry.client.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.main.client.AuthorizedConnectedAasRegistry; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.mongodb.KafkaEventsMongoDbStorageIntegrationTest.RegistrationEventKafkaListener; +import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.BaseIntegrationTest; +import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.EventQueue; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests the Authorized client + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=kafkaEvents,mongoDbStorage", "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092", "spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9096/realms/BaSyx", "basyx.feature.authorization.enabled=true", "basyx.feature.authorization.type=rbac", "basyx.feature.authorization.jwtBearerTokenProvider=keycloak", "basyx.feature.authorization.rbac.file=classpath:rbac_rules.json", "spring.data.mongodb.database=aasregistry", "spring.data.mongodb.uri=mongodb://mongoAdmin:mongoPassword@localhost:27017/"}) +public class AuthorizedClientTest extends BaseIntegrationTest { + + @Autowired + private RegistrationEventKafkaListener listener; + + @Value("${local.server.port}") + private int port; + + @Before + public void awaitAssignment() throws InterruptedException { + listener.awaitTopicAssignment(); + } + + @Override + public EventQueue queue() { + return listener.getQueue(); + } + + @Before + @Override + public void initClient() throws ApiException { + api = new AuthorizedConnectedAasRegistry("http://127.0.0.1:" + port, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + + api.deleteAllShellDescriptors(); + queue().assertNoAdditionalMessage(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + RegistryAndDiscoveryInterfaceApi registryApi = new AuthorizedConnectedAasRegistry("http://127.0.0.1:" + port, mockTokenManager); + + AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(); + descriptor.setIdShort("shortId"); + + ApiException exception = assertThrows(ApiException.class, () -> { + registryApi.postAssetAdministrationShellDescriptor(descriptor); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + @Test + @Override + public void whenPostSubmodelDescriptor_LocationIsReturned() throws ApiException, IOException { + // TODO: It uses normal GET unauthorized request, need to override and refactor + } + + @Test + @Override + public void whenPostShellDescriptor_LocationIsReturned() throws ApiException, IOException { + // TODO: It uses normal GET unauthorized request, need to override and refactor + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java index 8eb388ab6..0edaf75c5 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java @@ -63,7 +63,7 @@ public EventQueue queue() { @KafkaListener(topics = "aas-registry", batch = "false", groupId = "kafka-test", autoStartup = "true") @Component - private static class RegistrationEventKafkaListener implements ConsumerSeekAware { + public static class RegistrationEventKafkaListener implements ConsumerSeekAware { private final EventQueue queue; private final CountDownLatch latch = new CountDownLatch(1); @@ -75,6 +75,10 @@ private static class RegistrationEventKafkaListener implements ConsumerSeekAware public RegistrationEventKafkaListener(ObjectMapper mapper) { this.queue = new EventQueue(mapper); } + + public EventQueue getQueue() { + return this.queue; + } @KafkaHandler public void receiveMessage(String content) { diff --git a/basyx.aasrepository/basyx.aasrepository-client/pom.xml b/basyx.aasrepository/basyx.aasrepository-client/pom.xml index 52ec3a222..7a7ab42a5 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/pom.xml +++ b/basyx.aasrepository/basyx.aasrepository-client/pom.xml @@ -1,4 +1,4 @@ - 4.0.0 @@ -21,7 +21,7 @@ org.eclipse.digitaltwin.basyx basyx.aasrepository-core - + org.eclipse.digitaltwin.basyx basyx.aasrepository-core @@ -35,12 +35,45 @@ test tests + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.apache.httpcomponents.client5 + httpclient5 + test + org.eclipse.digitaltwin.basyx basyx.aasservice-core tests test + + + org.eclipse.digitaltwin.basyx + basyx.aasrepository-feature-authorization + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.aasrepository-feature-authorization + test + org.eclipse.digitaltwin.basyx basyx.aasrepository-http @@ -56,9 +89,20 @@ basyx.aasservice-backend-inmemory test + + org.mockito + mockito-core + test + org.eclipse.digitaltwin.basyx basyx.aasservice-client + + org.eclipse.digitaltwin.basyx + basyx.aasservice-client + test + tests + \ No newline at end of file diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java new file mode 100644 index 000000000..516dfcd0f --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/AuthorizedConnectedAasRepository.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasrepository.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.aasrepository.client.internal.AssetAdministrationShellRepositoryApi; +import org.eclipse.digitaltwin.basyx.aasservice.client.ConnectedAasService; +import org.eclipse.digitaltwin.basyx.aasservice.client.AuthorizedConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; + +/** + * Provides access to an Authorized Aas Repository on a remote server + * + * @author danish + */ +public class AuthorizedConnectedAasRepository extends ConnectedAasRepository { + + private TokenManager tokenManager; + + public AuthorizedConnectedAasRepository(String repoUrl, TokenManager tokenManager) { + super(repoUrl, new AssetAdministrationShellRepositoryApi(repoUrl, getRequestBuilder(tokenManager))); + this.tokenManager = tokenManager; + } + + public TokenManager getTokenManager() { + return tokenManager; + } + + @Override + public ConnectedAasService getConnectedAasService(String aasId) { + try { + getAas(aasId); + return new AuthorizedConnectedAasService(getAasUrl(aasId), tokenManager); + } catch (ApiException e) { + throw mapExceptionAasAccess(aasId, e); + } + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder().header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java index 370631eb4..c302adc52 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/ConnectedAasRepository.java @@ -66,6 +66,15 @@ public ConnectedAasRepository(String repoUrl) { this.aasRepoUrl = repoUrl; this.repoApi = new AssetAdministrationShellRepositoryApi(repoUrl); } + + public ConnectedAasRepository(String repoUrl, AssetAdministrationShellRepositoryApi assetAdministrationShellRepositoryApi) { + this.aasRepoUrl = repoUrl; + this.repoApi = assetAdministrationShellRepositoryApi; + } + + public String getBaseUrl() { + return aasRepoUrl; + } @Override public CursorResult> getAllAas(PaginationInfo pInfo) { @@ -165,7 +174,7 @@ public void deleteThumbnail(String aasId) { getConnectedAasService(aasId).deleteThumbnail(); } - private String getAasUrl(String aasId) { + protected String getAasUrl(String aasId) { return aasRepoUrl + "/shells/" + Base64UrlEncodedIdentifier.encodeIdentifier(aasId); } @@ -177,7 +186,7 @@ private RuntimeException mapExceptionAasUpdate(String aasId, ApiException e) { return mapExceptionAasAccess(aasId, e); } - private RuntimeException mapExceptionAasAccess(String aasId, ApiException e) { + protected RuntimeException mapExceptionAasAccess(String aasId, ApiException e) { if (e.getCode() == HttpStatus.NOT_FOUND.value()) { return new ElementDoesNotExistException(aasId); } else if (e.getCode() == HttpStatus.CONFLICT.value()) { diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java index 26289eb78..da33c612e 100644 --- a/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java +++ b/basyx.aasrepository/basyx.aasrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/client/internal/AssetAdministrationShellRepositoryApi.java @@ -64,19 +64,37 @@ public class AssetAdministrationShellRepositoryApi { private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public AssetAdministrationShellRepositoryApi() { this(new ApiClient()); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public AssetAdministrationShellRepositoryApi(HttpRequest.Builder httpRequestBuilder) { + this(); + this.httpRequestBuilder = httpRequestBuilder; } public AssetAdministrationShellRepositoryApi(ObjectMapper mapper, String baseUri) { this(new ApiClient(HttpClient.newBuilder(), mapper, baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public AssetAdministrationShellRepositoryApi(ObjectMapper mapper, String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(mapper, baseUri); + this.httpRequestBuilder = httpRequestBuilder; } public AssetAdministrationShellRepositoryApi(String baseUri) { this(new ApiClient(HttpClient.newBuilder(), new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()), baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public AssetAdministrationShellRepositoryApi(String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(baseUri); + this.httpRequestBuilder = httpRequestBuilder; } - public AssetAdministrationShellRepositoryApi(ApiClient apiClient) { memberVarHttpClient = apiClient.getHttpClient(); @@ -173,10 +191,10 @@ private HttpRequest.Builder deleteAssetAdministrationShellByIdRequestBuilder(Str throw new ApiException(400, "Missing the required parameter 'aasIdentifier' when calling deleteAssetAdministrationShellById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/shells/{aasIdentifier}" .replace("{aasIdentifier}", ApiClient.urlEncode(aasIdentifier.toString())); + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -344,9 +362,9 @@ private ApiResponse> private HttpRequest.Builder getAllAssetAdministrationShellsRequestBuilder(List assetIds, String idShort, Integer limit, String cursor) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/shells"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); List localVarQueryParams = new ArrayList<>(); StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); @@ -452,10 +470,10 @@ private HttpRequest.Builder getAssetAdministrationShellByIdRequestBuilder(String throw new ApiException(400, "Missing the required parameter 'aasIdentifier' when calling getAssetAdministrationShellById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/shells/{aasIdentifier}" .replace("{aasIdentifier}", ApiClient.urlEncode(aasIdentifier.toString())); + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -713,9 +731,9 @@ private HttpRequest.Builder postAssetAdministrationShellRequestBuilder(AssetAdmi throw new ApiException(400, "Missing the required parameter 'assetAdministrationShell' when calling postAssetAdministrationShell"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/shells"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -916,10 +934,10 @@ private HttpRequest.Builder putAssetAdministrationShellByIdRequestBuilder(String throw new ApiException(400, "Missing the required parameter 'assetAdministrationShell' when calling putAssetAdministrationShellById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/shells/{aasIdentifier}" .replace("{aasIdentifier}", ApiClient.urlEncode(aasIdentifier.toString())); + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/client/TestAuthorizedConnectedAasRepository.java b/basyx.aasrepository/basyx.aasrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/client/TestAuthorizedConnectedAasRepository.java new file mode 100644 index 000000000..aa6903e89 --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/client/TestAuthorizedConnectedAasRepository.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasrepository.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import java.io.FileNotFoundException; +import java.io.IOException; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepositorySuite; +import org.eclipse.digitaltwin.basyx.aasrepository.DummyAasFactory; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.DummyAuthorizedAasRepositoryComponent; +import org.eclipse.digitaltwin.basyx.aasservice.client.TestAuthorizedConnectedAasService; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Tests for {@link AuthorizedConnectedAasRepository} + * + * @author danish + */ +public class TestAuthorizedConnectedAasRepository extends AasRepositorySuite { + + private static ConfigurableApplicationContext appContext; + private static final String PROFILE = "authorization"; + + @BeforeClass + public static void startAASRepo() throws Exception { + SpringApplication application = new SpringApplication(DummyAuthorizedAasRepositoryComponent.class); + application.setAdditionalProfiles(PROFILE); + + appContext = application.run(new String[] {}); + } + + @After + public void removeAasFromRepo() throws FileNotFoundException, IOException { + TestAuthorizedConnectedAasService.configureSecurityContext(TestAuthorizedConnectedAasService.getTokenProvider()); + + AasRepository repo = appContext.getBean(AasRepository.class); + repo.getAllAas(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteAas); + + SecurityContextHolder.clearContext(); + } + + @AfterClass + public static void shutdownAASRepo() { + appContext.close(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + AasRepository aasRepository = new AuthorizedConnectedAasRepository("http://localhost:8081", mockTokenManager); + + AssetAdministrationShell expected = DummyAasFactory.createAasWithSubmodelReference(); + + ApiException exception = assertThrows(ApiException.class, () -> { + aasRepository.createAas(expected); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + @Override + protected AasRepository getAasRepository() { + return new AuthorizedConnectedAasRepository("http://localhost:8081", new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + } +} diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application-authorization.properties b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application-authorization.properties new file mode 100644 index 000000000..ebd881a6f --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application-authorization.properties @@ -0,0 +1,42 @@ +server.port=8081 +server.error.path=/error + +spring.application.name=AAS Repository +basyx.aasrepo.name=aas-repo + +basyx.backend = InMemory + +#basyx.aasrepository.feature.registryintegration=http://host.docker.internal:8050/api/v3.0 +#basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=aas +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + + +# basyx.aasrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = rbac +basyx.feature.authorization.jwtBearerTokenProvider = keycloak +basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx + +#################################################################################### +# Operation Delegation +#################################################################################### +# This feature is enabled by default + +#basyx.submodelrepository.feature.operation.delegation.enabled = false \ No newline at end of file diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application.properties b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application.properties new file mode 100644 index 000000000..1ffdef76b --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/application.properties @@ -0,0 +1,42 @@ +server.port=8080 +server.error.path=/error + +spring.application.name=AAS Repository +basyx.aasrepo.name=aas-repo + +basyx.backend = InMemory + +#basyx.aasrepository.feature.registryintegration=http://host.docker.internal:8050/api/v3.0 +#basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=aas +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + + +# basyx.aasrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +#basyx.feature.authorization.enabled = true +#basyx.feature.authorization.type = rbac +#basyx.feature.authorization.jwtBearerTokenProvider = keycloak +#basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +#spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx + +#################################################################################### +# Operation Delegation +#################################################################################### +# This feature is enabled by default + +#basyx.submodelrepository.feature.operation.delegation.enabled = false \ No newline at end of file diff --git a/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/rbac_rules.json b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..6bf880c8d --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-client/src/test/resources/rbac_rules.json @@ -0,0 +1,82 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": ["testAasId1", "specificAasId", "testAasId2"] + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, + { + "role": "basyx-asset-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-asset-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + } +] \ No newline at end of file diff --git a/basyx.aasservice/basyx.aasservice-client/pom.xml b/basyx.aasservice/basyx.aasservice-client/pom.xml index 3ed0c61ee..6e61a88b4 100644 --- a/basyx.aasservice/basyx.aasservice-client/pom.xml +++ b/basyx.aasservice/basyx.aasservice-client/pom.xml @@ -1,16 +1,18 @@ - - 4.0.0 - - org.eclipse.digitaltwin.basyx - basyx.aasservice - ${revision} - - basyx.aasservice-client - BaSyx AasServiceClient + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.aasservice + ${revision} + + basyx.aasservice-client + BaSyx AasServiceClient BaSyx AasServiceClient enabling interactions with a Aas Service - - + + org.eclipse.digitaltwin.aas4j aas4j-model @@ -44,18 +46,59 @@ tests test - + + org.eclipse.digitaltwin.basyx + basyx.aasrepository-feature-authorization + test + tests + + + + org.eclipse.digitaltwin.basyx + basyx.aasrepository-feature-authorization + test + + + org.mockito + mockito-core + test + + org.eclipse.digitaltwin.basyx basyx.aasrepository-http test + + org.apache.httpcomponents.client5 + httpclient5 + test + + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + org.eclipse.digitaltwin.basyx basyx.client - + junit junit diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java new file mode 100644 index 000000000..270f8d1b4 --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/AuthorizedConnectedAasService.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasservice.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.aasservice.client.internal.AssetAdministrationShellServiceApi; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; + +/** + * Provides access to an Authorized Aas Service on a remote server - regardless if it is + * hosted on a Aas Repository or standalone + * + * @author danish + */ +public class AuthorizedConnectedAasService extends ConnectedAasService { + + /** + * + * @param repoUrl + * the Url of the AAS Repository without the "/shells" part + * @param tokenManager + */ + public AuthorizedConnectedAasService(String repoUrl, TokenManager tokenManager) { + super(new AssetAdministrationShellServiceApi(repoUrl, getRequestBuilder(tokenManager))); + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder() + .header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java index 7f74f404e..b2ad3aa4a 100644 --- a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/ConnectedAasService.java @@ -57,6 +57,10 @@ public class ConnectedAasService implements AasService { public ConnectedAasService(String aasServiceUrl) { this.serviceApi = new AssetAdministrationShellServiceApi(aasServiceUrl); } + + public ConnectedAasService(AssetAdministrationShellServiceApi assetAdministrationShellServiceApi) { + this.serviceApi = assetAdministrationShellServiceApi; + } @Override public AssetAdministrationShell getAAS() throws ElementDoesNotExistException { diff --git a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java index 31b1dcd28..4b444cc5b 100644 --- a/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java +++ b/basyx.aasservice/basyx.aasservice-client/src/main/java/org/eclipse/digitaltwin/basyx/aasservice/client/internal/AssetAdministrationShellServiceApi.java @@ -73,17 +73,36 @@ public class AssetAdministrationShellServiceApi { private final Consumer memberVarInterceptor; private final Duration memberVarReadTimeout; private final Consumer> memberVarResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public AssetAdministrationShellServiceApi() { this(new ApiClient()); + this.httpRequestBuilder = HttpRequest.newBuilder(); } + + public AssetAdministrationShellServiceApi(HttpRequest.Builder httpRequestBuilder) { + this(); + this.httpRequestBuilder = httpRequestBuilder; +} public AssetAdministrationShellServiceApi(ObjectMapper mapper, String baseUri) { this(new ApiClient(HttpClient.newBuilder(), mapper, baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); } + public AssetAdministrationShellServiceApi(ObjectMapper mapper, String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(mapper, baseUri); + this.httpRequestBuilder = httpRequestBuilder; +} + public AssetAdministrationShellServiceApi(String baseUri) { this(new ApiClient(HttpClient.newBuilder(), new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()), baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public AssetAdministrationShellServiceApi(String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(baseUri); + this.httpRequestBuilder = httpRequestBuilder; } @@ -182,9 +201,9 @@ private HttpRequest.Builder deleteSubmodelReferenceByIdRequestBuilder(String sub throw new ApiException(400, "Missing the required parameter 'submodelIdentifier' when calling deleteSubmodelReferenceById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/submodel-refs/{submodelIdentifier}".replace("{submodelIdentifier}", ApiClient.urlEncode(submodelIdentifier.toString())); + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -265,9 +284,9 @@ public ApiResponse deleteThumbnailWithHttpInfoNoUrlEncoding() throws ApiEx private HttpRequest.Builder deleteThumbnailRequestBuilder() throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/asset-information/thumbnail"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -329,9 +348,9 @@ private ApiResponse>> getAllSubmode private HttpRequest.Builder getAllSubmodelReferencesRequestBuilder(Integer limit, String cursor) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/submodel-refs"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); List localVarQueryParams = new ArrayList<>(); StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); @@ -421,8 +440,8 @@ public ApiResponse getAssetAdministrationShellWithHttp private HttpRequest.Builder getAssetAdministrationShellRequestBuilder() throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); + localVarRequestBuilder.uri(URI.create(memberVarBaseUri)); localVarRequestBuilder.header("Accept", "application/json"); @@ -575,9 +594,9 @@ public ApiResponse getAssetInformationWithHttpInfoNoUrlEncodin private HttpRequest.Builder getAssetInformationRequestBuilder() throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/asset-information"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -729,9 +748,9 @@ public ApiResponse getThumbnailWithHttpInfoNoUrlEncoding() throws ApiExcep private HttpRequest.Builder getThumbnailRequestBuilder() throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/asset-information/thumbnail"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -814,9 +833,9 @@ private HttpRequest.Builder postSubmodelReferenceRequestBuilder(Reference refere throw new ApiException(400, "Missing the required parameter 'reference' when calling postSubmodelReference"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/submodel-refs"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -1002,9 +1021,9 @@ private HttpRequest.Builder putAssetInformationRequestBuilder(AssetInformation a throw new ApiException(400, "Missing the required parameter 'assetInformation' when calling putAssetInformation"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); - String localVarPath = "/asset-information"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); @@ -1068,8 +1087,11 @@ private ApiResponse putThumbnailWithHttpInfoNoUrlEncoding(String fileName, } private HttpRequest.Builder putThumbnailRequestBuilder(String fileName, ContentType contentType, InputStream inputStream) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + String localVarPath = "/asset-information/thumbnail"; + + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); + localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); localVarRequestBuilder.header("Accept", "application/json"); diff --git a/basyx.aasservice/basyx.aasservice-client/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/client/TestAuthorizedConnectedAasService.java b/basyx.aasservice/basyx.aasservice-client/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/client/TestAuthorizedConnectedAasService.java new file mode 100644 index 000000000..57a4d6180 --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/client/TestAuthorizedConnectedAasService.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasservice.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.interfaces.RSAPublicKey; + +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.basyx.aasrepository.AasRepository; +import org.eclipse.digitaltwin.basyx.aasrepository.DummyAasFactory; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.DummyAuthorizedAasRepositoryComponent; +import org.eclipse.digitaltwin.basyx.aasservice.AasService; +import org.eclipse.digitaltwin.basyx.aasservice.AasServiceSuite; +import org.eclipse.digitaltwin.basyx.authorization.AccessTokenProvider; +import org.eclipse.digitaltwin.basyx.authorization.DummyCredential; +import org.eclipse.digitaltwin.basyx.authorization.DummyCredentialStore; +import org.eclipse.digitaltwin.basyx.authorization.jwt.JwtTokenDecoder; +import org.eclipse.digitaltwin.basyx.authorization.jwt.PublicKeyUtils; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Tests for authorized ConnectedAasService + * + * @author danish + */ +public class TestAuthorizedConnectedAasService extends AasServiceSuite { + + private static final String AAS_REPO_URL = "http://localhost:8081/shells/"; + private static ConfigurableApplicationContext appContext; + private static final String PROFILE = "authorization"; + public static String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + public static String clientId = "basyx-client-api"; + + @BeforeClass + public static void startAASRepo() throws Exception { + SpringApplication application = new SpringApplication(DummyAuthorizedAasRepositoryComponent.class); + application.setAdditionalProfiles(PROFILE); + + appContext = application.run(new String[] {}); + } + + @After + public void removeAasFromRepo() { + AasRepository repo = appContext.getBean(AasRepository.class); + repo.getAllAas(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteAas); + } + + @AfterClass + public static void shutdownSubmodelService() { + appContext.close(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + createDummyShellOnRepo("dummyAasId"); + + AasService aasService = new AuthorizedConnectedAasService(AAS_REPO_URL + "dummyAasId", mockTokenManager); + + ApiException exception = assertThrows(ApiException.class, () -> { + aasService.getAAS(); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + private void createDummyShellOnRepo(String shellId) throws FileNotFoundException, IOException { + configureSecurityContext(getTokenProvider()); + + AasRepository repo = appContext.getBean(AasRepository.class); + + AssetAdministrationShell expected1 = DummyAasFactory.createAasWithSubmodelReference(); + expected1.setId(shellId); + + repo.createAas(expected1); + } + + @Override + protected AasService getAasService(AssetAdministrationShell shell) { + AasRepository repo = appContext.getBean(AasRepository.class); + repo.createAas(shell); + String base64UrlEncodedId = Base64UrlEncodedIdentifier.encodeIdentifier(shell.getId()); + return new AuthorizedConnectedAasService(AAS_REPO_URL + base64UrlEncodedId, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + } + + public static void configureSecurityContext(AccessTokenProvider accessTokenProvider) throws FileNotFoundException, IOException { + String adminToken = getAdminAccessToken(accessTokenProvider); + + String modulus = BaSyxHttpTestUtils.readJSONStringFromClasspath("authorization/modulus.txt"); + + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + public static String getAdminAccessToken(AccessTokenProvider tokenProvider) { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + public static AccessTokenProvider getTokenProvider() { + return new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + } +} diff --git a/basyx.aasservice/basyx.aasservice-client/src/test/resources/application-authorization.properties b/basyx.aasservice/basyx.aasservice-client/src/test/resources/application-authorization.properties new file mode 100644 index 000000000..ebd881a6f --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/test/resources/application-authorization.properties @@ -0,0 +1,42 @@ +server.port=8081 +server.error.path=/error + +spring.application.name=AAS Repository +basyx.aasrepo.name=aas-repo + +basyx.backend = InMemory + +#basyx.aasrepository.feature.registryintegration=http://host.docker.internal:8050/api/v3.0 +#basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=aas +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + + +# basyx.aasrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = rbac +basyx.feature.authorization.jwtBearerTokenProvider = keycloak +basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx + +#################################################################################### +# Operation Delegation +#################################################################################### +# This feature is enabled by default + +#basyx.submodelrepository.feature.operation.delegation.enabled = false \ No newline at end of file diff --git a/basyx.aasservice/basyx.aasservice-client/src/test/resources/application.properties b/basyx.aasservice/basyx.aasservice-client/src/test/resources/application.properties new file mode 100644 index 000000000..1ffdef76b --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/test/resources/application.properties @@ -0,0 +1,42 @@ +server.port=8080 +server.error.path=/error + +spring.application.name=AAS Repository +basyx.aasrepo.name=aas-repo + +basyx.backend = InMemory + +#basyx.aasrepository.feature.registryintegration=http://host.docker.internal:8050/api/v3.0 +#basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=aas +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + + +# basyx.aasrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +#basyx.feature.authorization.enabled = true +#basyx.feature.authorization.type = rbac +#basyx.feature.authorization.jwtBearerTokenProvider = keycloak +#basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +#spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx + +#################################################################################### +# Operation Delegation +#################################################################################### +# This feature is enabled by default + +#basyx.submodelrepository.feature.operation.delegation.enabled = false \ No newline at end of file diff --git a/basyx.aasservice/basyx.aasservice-client/src/test/resources/rbac_rules.json b/basyx.aasservice/basyx.aasservice-client/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..6bf880c8d --- /dev/null +++ b/basyx.aasservice/basyx.aasservice-client/src/test/resources/rbac_rules.json @@ -0,0 +1,82 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": ["testAasId1", "specificAasId", "testAasId2"] + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId" + } + }, + { + "role": "basyx-asset-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-asset-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "specificAasId-2" + } + } +] \ No newline at end of file diff --git a/basyx.common/basyx.client/pom.xml b/basyx.common/basyx.client/pom.xml index de0da1421..b751f6a23 100644 --- a/basyx.common/basyx.client/pom.xml +++ b/basyx.common/basyx.client/pom.xml @@ -1,9 +1,11 @@ - - 4.0.0 - - org.eclipse.digitaltwin.basyx - basyx.common - ${revision} + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.common + ${revision} basyx.client @@ -11,11 +13,11 @@ BaSyx Client Base - - org.eclipse.digitaltwin.basyx - basyx.http - - + + org.eclipse.digitaltwin.basyx + basyx.http + + com.fasterxml.jackson.core jackson-core @@ -49,6 +51,21 @@ jakarta.annotation-api provided + + + + org.json + json + 20240303 + + + + + com.nimbusds + oauth2-oidc-sdk + 11.12 + + \ No newline at end of file diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java new file mode 100644 index 000000000..b1a36a0ad --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/TokenManager.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization; + +import java.io.IOException; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.AccessTokenProvider; +import org.eclipse.digitaltwin.basyx.core.exceptions.AccessTokenRetrievalException; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.RefreshToken; + +/** + * Requests and manages the Access Tokens and Refresh Tokens. + * + * @author danish + */ +public class TokenManager { + + private String tokenEndpoint; + private AccessTokenProvider accessTokenProvider; + private String accessToken; + private String refreshToken; + private long accessTokenExpiryTime; + private long refreshTokenExpiryTime; + + public TokenManager(String tokenEndpoint, AccessTokenProvider accessTokenProvider) { + super(); + this.tokenEndpoint = tokenEndpoint; + this.accessTokenProvider = accessTokenProvider; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public AccessTokenProvider getAccessTokenProvider() { + return this.accessTokenProvider; + } + + /** + * Provides access token + * + * @return accessToken + * @throws IOException + */ + public synchronized String getAccessToken() throws IOException { + + if (accessToken != null && System.currentTimeMillis() < accessTokenExpiryTime) + return accessToken; + + if (refreshToken != null && System.currentTimeMillis() < refreshTokenExpiryTime) { + try { + return requestAccessToken(accessTokenProvider.getAccessTokenResponse(tokenEndpoint, refreshToken)); + } catch (IOException e) { + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + } + } + + try { + return requestAccessToken(accessTokenProvider.getAccessTokenResponse(tokenEndpoint)); + } catch (IOException e) { + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + } + } + + private String requestAccessToken(AccessTokenResponse accessTokenResponse) throws IOException { + AccessToken accessTokenObj = accessTokenResponse.getTokens().getAccessToken(); + accessToken = accessTokenObj.getValue(); + accessTokenExpiryTime = accessTokenObj.getLifetime(); + + RefreshToken refreshTokenObj = accessTokenResponse.getTokens().getRefreshToken(); + + if (refreshTokenObj != null) { + refreshToken = refreshTokenObj.getValue(); + refreshTokenExpiryTime = System.currentTimeMillis() + (30L * 24L * 60L * 60L * 1000L); + } + + return accessToken; + } + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/ClientCredential.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/ClientCredential.java new file mode 100644 index 000000000..519dcd793 --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/ClientCredential.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization.credential; + +/** + * A representational class for Client credential + * + * @author danish + */ +public class ClientCredential { + + private String clientId; + private String clientSecret; + + public ClientCredential(String clientId) { + super(); + this.clientId = clientId; + } + + public ClientCredential(String clientId, String clientSecret) { + this(clientId); + this.clientSecret = clientSecret; + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/PasswordCredential.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/PasswordCredential.java new file mode 100644 index 000000000..c16634f09 --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/credential/PasswordCredential.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization.credential; + +/** + * A representational class for Password credential + * + * @author danish + */ +public class PasswordCredential { + + private String username; + private String password; + + public PasswordCredential(String username, String password) { + super(); + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/AccessTokenProvider.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/AccessTokenProvider.java new file mode 100644 index 000000000..dac4a669f --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/AccessTokenProvider.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization.grant; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; + +/** + * An interface for access token providers with different flows. + * + * @author danish + */ +public interface AccessTokenProvider { + + /** + * Provides an access token response + * + * @param tokenEndpoint + * @return accessTokenResponse + */ + AccessTokenResponse getAccessTokenResponse(String tokenEndpoint); + + /** + * Provides an access token response + * + * @param tokenEndpoint + * @param refreshToken + * @return accessTokenResponse + */ + AccessTokenResponse getAccessTokenResponse(String tokenEndpoint, String refreshToken); + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/ClientCredentialAccessTokenProvider.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/ClientCredentialAccessTokenProvider.java new file mode 100644 index 000000000..6e8c7954e --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/ClientCredentialAccessTokenProvider.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization.grant; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.core.exceptions.AccessTokenRetrievalException; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.token.RefreshToken; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ClientCredentialsGrant; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.RefreshTokenGrant; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; + +/** + * Access token provider for the Client Credentials flow + * + * @author danish + */ +public class ClientCredentialAccessTokenProvider implements AccessTokenProvider { + + private final ClientCredential clientCredential; + private Collection scopes; + + public ClientCredentialAccessTokenProvider(ClientCredential clientCredential) { + this.clientCredential = clientCredential; + } + + public ClientCredentialAccessTokenProvider(ClientCredential clientCredential, Collection scopes) { + this(clientCredential); + this.scopes = scopes; + } + + @Override + public AccessTokenResponse getAccessTokenResponse(String tokenEndpoint) { + + if (isPublicClient()) + throw new RuntimeException("The client cannnot be public client with Password Grant"); + + AuthorizationGrant clientGrant = new ClientCredentialsGrant(); + + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(clientCredential.getClientId()), new Secret(clientCredential.getClientSecret())); + + URI tokenEndpointUri = getTokenEndpointUri(tokenEndpoint); + + TokenRequest request = new TokenRequest(tokenEndpointUri, clientAuth, clientGrant, Scope.parse(scopes)); + + return getTokenResponse(request); + } + + @Override + public AccessTokenResponse getAccessTokenResponse(String tokenEndpoint, String refreshToken) { + + if (isPublicClient()) + throw new RuntimeException("The client cannnot be public client with Password Grant"); + + RefreshToken refreshTokenObj = new RefreshToken(refreshToken); + + AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(refreshTokenObj); + + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(clientCredential.getClientId()), new Secret(clientCredential.getClientSecret())); + + URI tokenEndpointUri = getTokenEndpointUri(tokenEndpoint); + + TokenRequest request = new TokenRequest(tokenEndpointUri, clientAuth, refreshTokenGrant); + + return getTokenResponse(request); + } + + private AccessTokenResponse getTokenResponse(TokenRequest request) { + TokenResponse response; + + try { + response = TokenResponse.parse(request.toHTTPRequest().send()); + } catch (ParseException | IOException e) { + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + } + + if (!response.indicatesSuccess()) + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + response.toErrorResponse().toString()); + + return response.toSuccessResponse(); + } + + private boolean isPublicClient() { + return clientCredential.getClientSecret().isBlank(); + } + + private URI getTokenEndpointUri(String tokenEndpoint) { + try { + return new URI(tokenEndpoint); + } catch (URISyntaxException e) { + throw new RuntimeException(e.getMessage()); + } + } + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/PasswordCredentialAccessTokenProvider.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/PasswordCredentialAccessTokenProvider.java new file mode 100644 index 000000000..61fd00a90 --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/authorization/grant/PasswordCredentialAccessTokenProvider.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.authorization.grant; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.PasswordCredential; +import org.eclipse.digitaltwin.basyx.core.exceptions.AccessTokenRetrievalException; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.token.RefreshToken; +import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.RefreshTokenGrant; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; + +/** + * Access token provider for the Username Password Credentials flow + * + * @author danish + */ +public class PasswordCredentialAccessTokenProvider implements AccessTokenProvider { + + private final PasswordCredential passwordCredential; + private final ClientCredential clientCredential; + private Collection scopes; + + public PasswordCredentialAccessTokenProvider(PasswordCredential passwordCredential, ClientCredential clientCredential) { + this.passwordCredential = passwordCredential; + this.clientCredential = clientCredential; + } + + public PasswordCredentialAccessTokenProvider(PasswordCredential passwordCredential, ClientCredential clientCredential, Collection scopes) { + this(passwordCredential, clientCredential); + this.scopes = scopes; + } + + @Override + public AccessTokenResponse getAccessTokenResponse(String tokenEndpoint) { + + AuthorizationGrant passwordGrant = new ResourceOwnerPasswordCredentialsGrant(passwordCredential.getUsername(), new Secret(passwordCredential.getPassword())); + + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(clientCredential.getClientId()), new Secret(clientCredential.getClientSecret())); + + URI tokenEndpointUri = getTokenEndpointUri(tokenEndpoint); + + TokenRequest request = new TokenRequest(tokenEndpointUri, clientAuth, passwordGrant, Scope.parse(scopes)); + + return getTokenResponse(request); + } + + @Override + public AccessTokenResponse getAccessTokenResponse(String tokenEndpoint, String refreshToken) { + + RefreshToken refreshTokenObj = new RefreshToken(refreshToken); + + AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(refreshTokenObj); + + ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID(clientCredential.getClientId()), new Secret(clientCredential.getClientSecret())); + + URI tokenEndpointUri = getTokenEndpointUri(tokenEndpoint); + + TokenRequest request = new TokenRequest(tokenEndpointUri, clientAuth, refreshTokenGrant); + + return getTokenResponse(request); + } + + private AccessTokenResponse getTokenResponse(TokenRequest request) { + TokenResponse response; + + try { + response = TokenResponse.parse(request.toHTTPRequest().send()); + } catch (ParseException | IOException e) { + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + e.getMessage()); + } + + if (!response.indicatesSuccess()) + throw new AccessTokenRetrievalException("Error occurred while retrieving access token" + response.toErrorResponse().toString()); + + return response.toSuccessResponse(); + } + + private URI getTokenEndpointUri(String tokenEndpoint) { + try { + return new URI(tokenEndpoint); + } catch (URISyntaxException e) { + throw new RuntimeException(e.getMessage()); + } + } + +} diff --git a/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/resolver/DescriptorResolver.java b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/resolver/DescriptorResolver.java new file mode 100644 index 000000000..a9f8d0bdc --- /dev/null +++ b/basyx.common/basyx.client/src/main/java/org/eclipse/digitaltwin/basyx/client/internal/resolver/DescriptorResolver.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.client.internal.resolver; + +/** + * A generic interface for different kinds of resolvers for Descriptor. + * + * @author danish + */ +public interface DescriptorResolver { + + O resolveDescriptor(I descriptor); + +} diff --git a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/AccessTokenRetrievalException.java b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/AccessTokenRetrievalException.java new file mode 100644 index 000000000..b4e6ff1bc --- /dev/null +++ b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/AccessTokenRetrievalException.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.core.exceptions; + +/** + * Indicates failure while retrieving the access token + * + * @author danish + * + */ +@SuppressWarnings("serial") +public class AccessTokenRetrievalException extends RuntimeException { + + public AccessTokenRetrievalException() { + super(); + } + + public AccessTokenRetrievalException(String message) { + super(message); + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-client-native/pom.xml index cf3b06bd6..d59bdbe0b 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/pom.xml @@ -20,7 +20,6 @@ ${openapi.result.folder}/${openapi.name} - src/generated/java maven-clean-plugin @@ -130,6 +129,10 @@ jakarta.annotation jakarta.annotation-api + + org.eclipse.digitaltwin.basyx + basyx.client + diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/AuthorizedConnectedSubmodelRegistry.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/AuthorizedConnectedSubmodelRegistry.java new file mode 100644 index 000000000..5ea01059e --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/AuthorizedConnectedSubmodelRegistry.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelregistry.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; + +/** + * Provides access to an authorized Submodel Registry on a remote server + * + * @author danish + */ +public class AuthorizedConnectedSubmodelRegistry extends SubmodelRegistryApi { + + private final TokenManager tokenManager; + private final String submodelRegistryBasePath; + + public AuthorizedConnectedSubmodelRegistry(String basePath, TokenManager tokenManager) { + super(basePath, getRequestBuilder(tokenManager)); + this.submodelRegistryBasePath = basePath; + this.tokenManager = tokenManager; + } + + public String getBaseUrl() { + return submodelRegistryBasePath; + } + + public TokenManager getTokenManager() { + return tokenManager; + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder().header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/templates/libraries/native/api.mustache b/basyx.submodelregistry/basyx.submodelregistry-client-native/templates/libraries/native/api.mustache index 5ec50e316..d5d40059e 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/templates/libraries/native/api.mustache +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/templates/libraries/native/api.mustache @@ -59,17 +59,36 @@ public class {{classname}} { private final Duration memberVarReadTimeout; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarResponseInterceptor; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarAsyncResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public {{classname}}() { this(new ApiClient()); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(HttpRequest.Builder httpRequestBuilder) { + this(new ApiClient()); + this.httpRequestBuilder = httpRequestBuilder; } public {{classname}}(String protocol, String host, int port) { this(protocol + "://" + host + ":" + port); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(String protocol, String host, int port, HttpRequest.Builder httpRequestBuilder) { + this(protocol + "://" + host + ":" + port); + this.httpRequestBuilder = httpRequestBuilder; } public {{classname}}(String basePath) { this(withBaseUri(new ApiClient(), basePath)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public {{classname}}(String basePath, HttpRequest.Builder httpRequestBuilder) { + this(withBaseUri(new ApiClient(), basePath)); + this.httpRequestBuilder = httpRequestBuilder; } private static ApiClient withBaseUri(ApiClient client, String uri) { @@ -361,7 +380,7 @@ public class {{classname}} { {{#hasPathParams}}{{#pathParams}}{{#vendorExtensions.x-utf8-base64-url-encoded-as-string}}String {{{paramName}}}AsBase64EncodedParam = {{{paramName}}} == null ? null : new String(java.util.Base64.getUrlEncoder().encode({{paramName}}.getBytes(java.nio.charset.StandardCharsets.UTF_8)), java.nio.charset.StandardCharsets.UTF_8); {{/vendorExtensions.x-utf8-base64-url-encoded-as-string}}{{/pathParams}}{{/hasPathParams}} - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); {{! Switch delimiters for baseName so we can write constants like "{query}" }} String localVarPath = "{{{path}}}"{{#pathParams}} diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java new file mode 100644 index 000000000..a904ca815 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedClientTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.ApiException; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.AuthorizedConnectedSubmodelRegistry; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.api.SubmodelRegistryApi; +import org.eclipse.digitaltwin.basyx.submodelregistry.client.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.mongodb.KafkaEventsMongoDbStorageIntegrationTest.RegistrationEventKafkaListener; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.BaseIntegrationTest; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.EventQueue; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests the Authorized client + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=kafkaEvents,mongoDbStorage", "spring.kafka.bootstrap-servers=PLAINTEXT_HOST://localhost:9092", "spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9096/realms/BaSyx", "basyx.feature.authorization.enabled=true", "basyx.feature.authorization.type=rbac", "basyx.feature.authorization.jwtBearerTokenProvider=keycloak", "basyx.feature.authorization.rbac.file=classpath:rbac_rules.json", "spring.data.mongodb.database=submodelregistry", "spring.data.mongodb.uri=mongodb://mongoAdmin:mongoPassword@localhost:27017/"}) +public class AuthorizedClientTest extends BaseIntegrationTest { + + @Autowired + private RegistrationEventKafkaListener listener; + + @Value("${local.server.port}") + private int port; + + @Before + public void awaitAssignment() throws InterruptedException { + listener.awaitTopicAssignment(); + } + + @Override + public EventQueue queue() { + return listener.getQueue(); + } + + @Before + @Override + public void initClient() throws ApiException { + api = new AuthorizedConnectedSubmodelRegistry("http://127.0.0.1:" + port, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + api.deleteAllSubmodelDescriptors(); + queue().assertNoAdditionalMessage(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + SubmodelRegistryApi registryApi = new AuthorizedConnectedSubmodelRegistry("http://127.0.0.1:" + port, mockTokenManager); + + SubmodelDescriptor descriptor = new SubmodelDescriptor(); + descriptor.setIdShort("shortId"); + + ApiException exception = assertThrows(ApiException.class, () -> { + registryApi.postSubmodelDescriptor(descriptor); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + @Test + @Override + public void whenPostSubmodelDescriptor_LocationIsReturned() throws ApiException, IOException { + // TODO: It uses normal GET unauthorized request, need to override and refactor + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java index 201815e49..0bb275662 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/KafkaEventsMongoDbStorageIntegrationTest.java @@ -64,7 +64,7 @@ public EventQueue queue() { @KafkaListener(topics = "submodel-registry", batch = "false", groupId = "kafka-test", autoStartup = "true" ) @Component - private static class RegistrationEventKafkaListener implements ConsumerSeekAware { + public static class RegistrationEventKafkaListener implements ConsumerSeekAware { private final EventQueue queue; private final CountDownLatch latch = new CountDownLatch(1); @@ -81,6 +81,10 @@ public RegistrationEventKafkaListener(ObjectMapper mapper) { public void receiveMessage(String content) { queue.offer(content); } + + public EventQueue getQueue() { + return this.queue; + } @Override public void onPartitionsAssigned(Map assignments, diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/pom.xml b/basyx.submodelrepository/basyx.submodelrepository-client/pom.xml index aa48feed0..e4001b476 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/pom.xml +++ b/basyx.submodelrepository/basyx.submodelrepository-client/pom.xml @@ -25,6 +25,11 @@ org.eclipse.digitaltwin.basyx basyx.submodelrepository-core + + + org.eclipse.digitaltwin.basyx + basyx.client + org.eclipse.digitaltwin.basyx @@ -69,6 +74,12 @@ org.eclipse.digitaltwin.basyx basyx.submodelservice-client + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-client + test + tests + junit @@ -85,5 +96,44 @@ basyx.submodelservice-backend-inmemory test + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-feature-authorization + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-feature-authorization + test + + + org.mockito + mockito-core + test + \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java new file mode 100644 index 000000000..65cd7c83c --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/AuthorizedConnectedSubmodelRepository.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelrepository.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.submodelrepository.client.internal.SubmodelRepositoryApi; +import org.eclipse.digitaltwin.basyx.submodelservice.client.AuthorizedConnectedSubmodelService; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; + +/** + * Provides access to an Authorized Submodel Repository on a remote server + * + * @author danish + */ +public class AuthorizedConnectedSubmodelRepository extends ConnectedSubmodelRepository { + + private TokenManager tokenManager; + + public AuthorizedConnectedSubmodelRepository(String repoUrl, TokenManager tokenManager) { + super(repoUrl, new SubmodelRepositoryApi(repoUrl, getRequestBuilder(tokenManager))); + this.tokenManager = tokenManager; + } + + public TokenManager getTokenManager() { + return tokenManager; + } + + @Override + public ConnectedSubmodelService getConnectedSubmodelService(String submodelId) { + try { + getSubmodel(submodelId); + return new AuthorizedConnectedSubmodelService(getSubmodelUrl(submodelId), tokenManager); + } catch (ApiException e) { + throw mapExceptionSubmodelAccess(getSubmodelUrl(submodelId), e); + } + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder() + .header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java index e7589bf51..a4d10f14f 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java @@ -71,6 +71,15 @@ public ConnectedSubmodelRepository(String submodelRepoUrl) { this.repoApi = new SubmodelRepositoryApi(submodelRepoUrl); this.submodelRepoUrl = submodelRepoUrl; } + + public ConnectedSubmodelRepository(String submodelRepoUrl, SubmodelRepositoryApi submodelRepositoryApi) { + this.submodelRepoUrl = submodelRepoUrl; + this.repoApi = submodelRepositoryApi; + } + + public String getBaseUrl() { + return submodelRepoUrl; + } /** * Retrieves the Submodel with the specific id @@ -216,11 +225,11 @@ public void deleteFileValue(String submodelId, String idShortPath) throws Elemen getConnectedSubmodelService(submodelId).deleteFileValue(idShortPath); } - private String getSubmodelUrl(String submodelId) { + protected String getSubmodelUrl(String submodelId) { return submodelRepoUrl + "/submodels/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); } - private RuntimeException mapExceptionSubmodelAccess(String submodelId, ApiException e) { + protected RuntimeException mapExceptionSubmodelAccess(String submodelId, ApiException e) { if (e.getCode() == HttpStatus.NOT_FOUND.value()) { return new ElementDoesNotExistException(submodelId); } else if (e.getCode() == HttpStatus.CONFLICT.value()) { diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java index fa0f78180..88e0e9140 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/internal/SubmodelRepositoryApi.java @@ -43,6 +43,7 @@ import org.eclipse.digitaltwin.basyx.client.internal.ApiException; import org.eclipse.digitaltwin.basyx.client.internal.ApiResponse; import org.eclipse.digitaltwin.basyx.client.internal.Pair; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; import org.eclipse.digitaltwin.basyx.http.pagination.Base64UrlEncodedCursorResult; @@ -61,17 +62,35 @@ public class SubmodelRepositoryApi { private final Duration memberVarReadTimeout; private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public SubmodelRepositoryApi() { this(new ApiClient()); } + + public SubmodelRepositoryApi(HttpRequest.Builder httpRequestBuilder) { + this(); + this.httpRequestBuilder = httpRequestBuilder; + } public SubmodelRepositoryApi(ObjectMapper mapper, String baseUri) { this(new ApiClient(HttpClient.newBuilder(), mapper, baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public SubmodelRepositoryApi(ObjectMapper mapper, String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(mapper, baseUri); + this.httpRequestBuilder = httpRequestBuilder; } public SubmodelRepositoryApi(String baseUri) { this(new ApiClient(HttpClient.newBuilder(), new JsonMapperFactory().create(new SimpleAbstractTypeResolverFactory().create()), baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public SubmodelRepositoryApi(String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(baseUri); + this.httpRequestBuilder = httpRequestBuilder; } public SubmodelRepositoryApi(ApiClient apiClient) { @@ -183,7 +202,7 @@ private HttpRequest.Builder getSubmodelByIdRequestBuilder(String submodelIdentif throw new ApiException(400, "Missing the required parameter 'submodelIdentifier' when calling getSubmodelById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodels/{submodelIdentifier}".replace("{submodelIdentifier}", ApiClient.urlEncode(submodelIdentifier.toString())); @@ -287,7 +306,7 @@ private HttpRequest.Builder postSubmodelRequestBuilder(Submodel submodel) throws throw new ApiException(400, "Missing the required parameter 'submodel' when calling postSubmodel"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodels"; @@ -391,7 +410,7 @@ private HttpRequest.Builder putSubmodelByIdRequestBuilder(String submodelIdentif throw new ApiException(400, "Missing the required parameter 'submodel' when calling putSubmodelById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodels/{submodelIdentifier}".replace("{submodelIdentifier}", ApiClient.urlEncode(submodelIdentifier.toString())); @@ -485,7 +504,7 @@ private HttpRequest.Builder deleteSubmodelByIdRequestBuilder(String submodelIden throw new ApiException(400, "Missing the required parameter 'submodelIdentifier' when calling deleteSubmodelById"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodels/{submodelIdentifier}".replace("{submodelIdentifier}", ApiClient.urlEncode(submodelIdentifier.toString())); @@ -560,7 +579,7 @@ private ApiResponse>> getAllSubmodel private HttpRequest.Builder getAllSubmodelsRequestBuilder(String semanticId, String idShort, Integer limit, String cursor, String level, String extent) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodels"; diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestAuthorizedConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestAuthorizedConnectedSubmodelRepository.java new file mode 100644 index 000000000..a6392f36f --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestAuthorizedConnectedSubmodelRepository.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelrepository.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.core.exceptions.MissingIdentifierException; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.core.SubmodelRepositorySuite; +import org.eclipse.digitaltwin.basyx.submodelrepository.feature.authorization.DummyAuthorizedSubmodelRepositoryComponent; +import org.eclipse.digitaltwin.basyx.submodelservice.DummySubmodelFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.client.TestAuthorizedConnectedSubmodelService; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Tests for {@link AuthorizedConnectedSubmodelRepository} + * + * @author danish + */ +public class TestAuthorizedConnectedSubmodelRepository extends SubmodelRepositorySuite { + + private static final String SUBMODEL_REPO_URL = "http://localhost:8081"; + private static ConfigurableApplicationContext appContext; + private static final String PROFILE = "authorization"; + + @BeforeClass + public static void startAASRepo() throws Exception { + SpringApplication application = new SpringApplication(DummyAuthorizedSubmodelRepositoryComponent.class); + application.setAdditionalProfiles(PROFILE); + + appContext = application.run(new String[] {}); + } + + @After + public void removeSubmodelFromRepo() throws FileNotFoundException, IOException { + TestAuthorizedConnectedSubmodelService.configureSecurityContext(TestAuthorizedConnectedSubmodelService.getTokenProvider()); + + SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + + SecurityContextHolder.clearContext(); + } + + @AfterClass + public static void shutdownAASRepo() { + appContext.close(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + SubmodelRepository submodelRepository = new AuthorizedConnectedSubmodelRepository(SUBMODEL_REPO_URL, mockTokenManager); + + Submodel expected = DummySubmodelFactory.createSimpleDataSubmodel(); + + ApiException exception = assertThrows(ApiException.class, () -> { + submodelRepository.createSubmodel(expected); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + @Override + protected ConnectedSubmodelRepository getSubmodelRepository() { + return new AuthorizedConnectedSubmodelRepository(SUBMODEL_REPO_URL, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + } + + @Override + protected ConnectedSubmodelRepository getSubmodelRepository(Collection submodels) { + try { + TestAuthorizedConnectedSubmodelService.configureSecurityContext(TestAuthorizedConnectedSubmodelService.getTokenProvider()); + } catch (IOException e) { + e.printStackTrace(); + } + + SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); + submodels.forEach(repo::createSubmodel); + + SecurityContextHolder.clearContext(); + + return getSubmodelRepository(); + } + + @Override + @Test(expected = MissingIdentifierException.class) + public void updateExistingSubmodelWithMismatchId() { + // TODO There should be a way to differentiate between both exceptions through + // the Http response + super.updateExistingSubmodelWithMismatchId(); + } + + @Override + protected boolean fileExistsInStorage(String fileValue) { + java.io.File file = new java.io.File(fileValue); + + return file.exists(); + } +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java index 4ee14b0b7..f1f4065ef 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/TestConnectedSubmodelRepository.java @@ -77,7 +77,7 @@ public void getConnectedSubmodelServiceNonExistingSubmodel() { @Override protected ConnectedSubmodelRepository getSubmodelRepository() { - return new ConnectedSubmodelRepository("http://localhost:8080"); + return new ConnectedSubmodelRepository("http://localhost:8081"); } @Override diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application-authorization.properties b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application-authorization.properties new file mode 100644 index 000000000..7e32a7c95 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application-authorization.properties @@ -0,0 +1,33 @@ +server.port=8081 + +spring.application.name=Submodel Repository +basyx.smrepo.name = sm-repo +basyx.backend = InMemory + +# basyx.submodelrepository.feature.registryintegration=http://localhost:8060/api/v3.0 +# basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=mongo +# or spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=submodels +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + +# basyx.submodelrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = rbac +basyx.feature.authorization.jwtBearerTokenProvider = keycloak +basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application.properties b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application.properties new file mode 100644 index 000000000..fdc1f0c47 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/application.properties @@ -0,0 +1,33 @@ +server.port=8081 + +spring.application.name=Submodel Repository +basyx.smrepo.name = sm-repo +basyx.backend = InMemory + +# basyx.submodelrepository.feature.registryintegration=http://localhost:8060/api/v3.0 +# basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=mongo +# or spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=submodels +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + +# basyx.submodelrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +#basyx.feature.authorization.enabled = true +#basyx.feature.authorization.type = rbac +#basyx.feature.authorization.jwtBearerTokenProvider = keycloak +#basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +#spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/rbac_rules.json b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..ef11e18f6 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/test/resources/rbac_rules.json @@ -0,0 +1,155 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE", "EXECUTE"], + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": ["specificSubmodelId", "testSMId1", "testSMId2"], + "submodelElementIdShortPaths": ["testSMEIdShortPath1","smc2.specificSubmodelElementIdShort","testSMEIdShortPath2"] + } + }, + { + "role": "basyx-sme-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-sme-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2" + } + }, + { + "role": "basyx-sme-updater-three", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc1.specificSubmodelElementIdShort-2" + } + }, + { + "role": "basyx-file-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor-two", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "square" + } + }, + { + "role": "basyx-file-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + } +] \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-client/pom.xml b/basyx.submodelservice/basyx.submodelservice-client/pom.xml index c13b742ec..65ec41d68 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/pom.xml +++ b/basyx.submodelservice/basyx.submodelservice-client/pom.xml @@ -55,5 +55,48 @@ basyx.submodelservice-backend-inmemory test + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-feature-authorization + test + tests + + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-feature-authorization + test + + + + org.mockito + mockito-core + test + \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java new file mode 100644 index 000000000..549d02881 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/AuthorizedConnectedSubmodelService.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelservice.client; + +import java.io.IOException; +import java.net.http.HttpRequest; + +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.submodelservice.client.internal.SubmodelServiceApi; + +/** + * Provides access to an authorized Submodel Service on a remote server - regardless if it + * is hosted on a Submodel Repository or standalone + * + * @author danish + */ +public class AuthorizedConnectedSubmodelService extends ConnectedSubmodelService { + + /** + * + * @param repoUrl + * the Url of the Submodel Repository without the "/submodels" part + * @param tokenManager + */ + public AuthorizedConnectedSubmodelService(String repoUrl, TokenManager tokenManager) { + super(new SubmodelServiceApi(repoUrl, getRequestBuilder(tokenManager))); + } + + private static HttpRequest.Builder getRequestBuilder(TokenManager tokenManager) { + try { + return HttpRequest.newBuilder() + .header("Authorization", "Bearer " + tokenManager.getAccessToken()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Unable to request access token"); + } + } + +} diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java index 3fb627832..df7baf690 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java @@ -73,6 +73,11 @@ public ConnectedSubmodelService(String submodelServiceUrl) { this.serviceApi = new SubmodelServiceApi(submodelServiceUrl); this.submodelElementValueMapperFactory = new SubmodelElementValueMapperFactory(); } + + public ConnectedSubmodelService(SubmodelServiceApi submodelServiceApi) { + this.serviceApi = submodelServiceApi; + this.submodelElementValueMapperFactory = new SubmodelElementValueMapperFactory(); + } @Override public Submodel getSubmodel() { diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java index fc5280e0c..73eb5af24 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/internal/SubmodelServiceApi.java @@ -74,18 +74,37 @@ public class SubmodelServiceApi { private final Duration memberVarReadTimeout; private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; + private HttpRequest.Builder httpRequestBuilder; public SubmodelServiceApi() { this(new ApiClient()); -} + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public SubmodelServiceApi(HttpRequest.Builder httpRequestBuilder) { + this(); + this.httpRequestBuilder = httpRequestBuilder; + } public SubmodelServiceApi(ObjectMapper mapper, String baseUri) { this(new ApiClient(HttpClient.newBuilder(), mapper, baseUri)); -} + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public SubmodelServiceApi(ObjectMapper mapper, String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(mapper, baseUri); + this.httpRequestBuilder = httpRequestBuilder; + } -public SubmodelServiceApi(String baseUri) { - this(new ApiClient(HttpClient.newBuilder(), new SubmodelSpecificJsonMapperFactory().create(), baseUri)); -} + public SubmodelServiceApi(String baseUri) { + this(new ApiClient(HttpClient.newBuilder(), new SubmodelSpecificJsonMapperFactory().create(), baseUri)); + this.httpRequestBuilder = HttpRequest.newBuilder(); + } + + public SubmodelServiceApi(String baseUri, HttpRequest.Builder httpRequestBuilder) { + this(baseUri); + this.httpRequestBuilder = httpRequestBuilder; + } @@ -189,7 +208,7 @@ public ApiResponse getSubmodelWithHttpInfoNoUrlEncoding(String level, private HttpRequest.Builder getSubmodelRequestBuilder(String level, String extent) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = ""; @@ -311,7 +330,7 @@ private HttpRequest.Builder getSubmodelElementByPathRequestBuilder(String idShor throw new ApiException(400, "Missing the required parameter 'idShortPath' when calling getSubmodelElementByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -436,7 +455,7 @@ private HttpRequest.Builder getSubmodelElementByPathValueOnlyRequestBuilder(Stri throw new ApiException(400, "Missing the required parameter 'idShortPath' when calling getSubmodelElementByPathValueOnly"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/$value".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -577,7 +596,7 @@ private HttpRequest.Builder patchSubmodelElementByPathValueOnlyRequestBuilder(St throw new ApiException(400, "Missing the required parameter 'getSubmodelElementsValueResult' when calling patchSubmodelElementByPathValueOnly"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/$value".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -688,7 +707,7 @@ private HttpRequest.Builder postSubmodelElementRequestBuilder(SubmodelElement su throw new ApiException(400, "Missing the required parameter 'submodelElement' when calling postSubmodelElement"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements"; @@ -793,7 +812,7 @@ private HttpRequest.Builder postSubmodelElementByPathRequestBuilder(String idSho throw new ApiException(400, "Missing the required parameter 'submodelElement' when calling postSubmodelElementByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -908,7 +927,7 @@ private HttpRequest.Builder putSubmodelElementByPathRequestBuilder(String idShor throw new ApiException(400, "Missing the required parameter 'submodelElement' when calling putSubmodelElementByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -1019,7 +1038,7 @@ private HttpRequest.Builder deleteSubmodelElementByPathRequestBuilder(String idS throw new ApiException(400, "Missing the required parameter 'idShortPath' when calling deleteSubmodelElementByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -1090,7 +1109,7 @@ private ApiResponse>> getAllS private HttpRequest.Builder getAllSubmodelElementsRequestBuilder(Integer limit, String cursor, String level, String extent) throws ApiException { - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements"; @@ -1179,7 +1198,7 @@ private HttpRequest.Builder putFileByPathRequestBuilder(String idShortPath, Stri if (inputStream == null) throw new ApiException(400, "Missing the required parameter 'inputStream' when calling putFileByPath"); - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/attachment".replace("{idShortPath}", ApiClient.urlEncode(idShortPath)); @@ -1288,7 +1307,7 @@ private HttpRequest.Builder deleteFileByPathRequestBuilder(String idShortPath) t throw new ApiException(400, "Missing the required parameter 'idShortPath' when calling deleteFileByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/attachment".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -1355,7 +1374,7 @@ private HttpRequest.Builder getFileByPathRequestBuilder(String idShortPath) thro throw new ApiException(400, "Missing the required parameter 'idShortPath' when calling getFileByPath"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/attachment".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); @@ -1452,7 +1471,7 @@ private HttpRequest.Builder invokeOperationRequestBuilder(String idShortPath, Op throw new ApiException(400, "Missing the required parameter 'operationRequest' when calling invokeOperation"); } - HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + HttpRequest.Builder localVarRequestBuilder = this.httpRequestBuilder.copy(); String localVarPath = "/submodel-elements/{idShortPath}/invoke".replace("{idShortPath}", ApiClient.urlEncode(idShortPath.toString())); diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestAuthorizedConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestAuthorizedConnectedSubmodelService.java new file mode 100644 index 000000000..02e3487d2 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestAuthorizedConnectedSubmodelService.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelservice.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.interfaces.RSAPublicKey; + +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.basyx.authorization.AccessTokenProvider; +import org.eclipse.digitaltwin.basyx.authorization.DummyCredential; +import org.eclipse.digitaltwin.basyx.authorization.DummyCredentialStore; +import org.eclipse.digitaltwin.basyx.authorization.jwt.JwtTokenDecoder; +import org.eclipse.digitaltwin.basyx.authorization.jwt.PublicKeyUtils; +import org.eclipse.digitaltwin.basyx.client.internal.ApiException; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.credential.ClientCredential; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.ClientCredentialAccessTokenProvider; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; +import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; +import org.eclipse.digitaltwin.basyx.submodelrepository.feature.authorization.DummyAuthorizedSubmodelRepositoryComponent; +import org.eclipse.digitaltwin.basyx.submodelservice.DummySubmodelFactory; +import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService; +import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceSuite; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Tests the authorized Connected Submodel Service + * + * @author danish + */ +public class TestAuthorizedConnectedSubmodelService extends SubmodelServiceSuite { + + private static final String SUBMODEL_REPO_URL = "http://localhost:8081/submodels/"; + private static ConfigurableApplicationContext appContext; + private static final String PROFILE = "authorization"; + public static String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + public static String clientId = "basyx-client-api"; + + @BeforeClass + public static void startAASRepo() throws Exception { + SpringApplication application = new SpringApplication(DummyAuthorizedSubmodelRepositoryComponent.class); + application.setAdditionalProfiles(PROFILE); + + appContext = application.run(new String[] {}); + } + + @After + public void removeAasFromRepo() { + SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); + repo.getAllSubmodels(PaginationInfo.NO_LIMIT).getResult().stream().map(s -> s.getId()).forEach(repo::deleteSubmodel); + } + + @AfterClass + public static void shutdownSubmodelService() { + appContext.close(); + } + + @Test + public void sendUnauthorizedRequest() throws IOException { + TokenManager mockTokenManager = Mockito.mock(TokenManager.class); + + Mockito.when(mockTokenManager.getAccessToken()).thenReturn("mockedAccessToken"); + + createDummyShellOnRepo("dummySubmodelId"); + + SubmodelService submodelService = new AuthorizedConnectedSubmodelService(SUBMODEL_REPO_URL + "dummySubmodelId", mockTokenManager); + + ApiException exception = assertThrows(ApiException.class, () -> { + submodelService.getSubmodel(); + }); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), exception.getCode()); + } + + private void createDummyShellOnRepo(String shellId) throws FileNotFoundException, IOException { + configureSecurityContext(getTokenProvider()); + + SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); + + Submodel expected1 = DummySubmodelFactory.createSimpleDataSubmodel(); + expected1.setId(shellId); + + repo.createSubmodel(expected1); + } + + @Override + protected SubmodelService getSubmodelService(Submodel submodel) { + SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); + repo.createSubmodel(submodel); + String base64UrlEncodedId = Base64UrlEncodedIdentifier.encodeIdentifier(submodel.getId()); + return new AuthorizedConnectedSubmodelService(SUBMODEL_REPO_URL + base64UrlEncodedId, new TokenManager("http://localhost:9096/realms/BaSyx/protocol/openid-connect/token", new ClientCredentialAccessTokenProvider(new ClientCredential("workstation-1", "nY0mjyECF60DGzNmQUjL81XurSl8etom")))); + } + + @Override + protected boolean fileExistsInStorage(String fileValue) { + java.io.File file = new java.io.File(fileValue); + + return file.exists(); + } + + public static void configureSecurityContext(AccessTokenProvider accessTokenProvider) throws FileNotFoundException, IOException { + String adminToken = getAdminAccessToken(accessTokenProvider); + + String modulus = BaSyxHttpTestUtils.readJSONStringFromClasspath("authorization/modulus.txt"); + + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + public static String getAdminAccessToken(AccessTokenProvider tokenProvider) { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + public static AccessTokenProvider getTokenProvider() { + return new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + } + +} diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelElements.java b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelElements.java index d3d4f5648..1860d833f 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelElements.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelElements.java @@ -104,7 +104,7 @@ public class TestConnectedSubmodelElements { private static final String EXPECTED_STRING = "This is a test"; private static final String SUBMODEL_ID_SHORT = "submodelIdShort"; private static final String SUBMODEL_ID = "submodelId"; - private static final String ACCESS_URL = "http://localhost:8080/submodels/"; + private static final String ACCESS_URL = "http://localhost:8081/submodels/"; private static ConfigurableApplicationContext appContext; @BeforeClass diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java index d14c32b2a..fa997b7ce 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/client/TestConnectedSubmodelService.java @@ -71,7 +71,7 @@ protected SubmodelService getSubmodelService(Submodel submodel) { SubmodelRepository repo = appContext.getBean(SubmodelRepository.class); repo.createSubmodel(submodel); String base64UrlEncodedId = Base64UrlEncodedIdentifier.encodeIdentifier(submodel.getId()); - return new ConnectedSubmodelService("http://localhost:8080/submodels/" + base64UrlEncodedId); + return new ConnectedSubmodelService("http://localhost:8081/submodels/" + base64UrlEncodedId); } @Override diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application-authorization.properties b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application-authorization.properties new file mode 100644 index 000000000..7e32a7c95 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application-authorization.properties @@ -0,0 +1,33 @@ +server.port=8081 + +spring.application.name=Submodel Repository +basyx.smrepo.name = sm-repo +basyx.backend = InMemory + +# basyx.submodelrepository.feature.registryintegration=http://localhost:8060/api/v3.0 +# basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=mongo +# or spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=submodels +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + +# basyx.submodelrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = rbac +basyx.feature.authorization.jwtBearerTokenProvider = keycloak +basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application.properties b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application.properties new file mode 100644 index 000000000..fdc1f0c47 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/application.properties @@ -0,0 +1,33 @@ +server.port=8081 + +spring.application.name=Submodel Repository +basyx.smrepo.name = sm-repo +basyx.backend = InMemory + +# basyx.submodelrepository.feature.registryintegration=http://localhost:8060/api/v3.0 +# basyx.externalurl=http://localhost:8081 + +#basyx.backend = MongoDB +#spring.data.mongodb.host=mongo +# or spring.data.mongodb.host=127.0.0.1 +#spring.data.mongodb.port=27017 +#spring.data.mongodb.database=submodels +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.username=mongoAdmin +#spring.data.mongodb.password=mongoPassword + +# basyx.submodelrepository.feature.mqtt.enabled = true +# mqtt.clientId=TestClient +# mqtt.hostname = localhost +# mqtt.port = 1883 + +# basyx.cors.allowed-origins=http://localhost:3000, http://localhost:4000 + +#################################################################################### +# Authorization +#################################################################################### +#basyx.feature.authorization.enabled = true +#basyx.feature.authorization.type = rbac +#basyx.feature.authorization.jwtBearerTokenProvider = keycloak +#basyx.feature.authorization.rbac.file = classpath:rbac_rules.json +#spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/authorization/modulus.txt b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/authorization/modulus.txt @@ -0,0 +1 @@ +sQ4RUvlZXLjQpLizIMht46ASwpwUoPpUDTmUhxF_VV-ezHTHbTAdv_5RA1GgCyTjAQe_Ih6dLByZrJFaroyvqgIJdMRCb0MwajI1US0_NwHtVvo5dea_-GKeHGRzvYZjxVlooR_1xmskfAM_NR_NaOMUhr_TNV7n7LXEb06L55DYnqdqrUnhXLewBq1lo54GsMqxN4hlkc4nJ2uYUtWEkV4SlMyYRjXBlylpuWFO0-_FsmaqSx7CZWjNWmqKlxnvUgRrT5Vh-9ZgCAOmGtLuFYOVWzqmVjWtyiJ1pYfWjwc86XeWBFefVY1lkoNNoSYKV4AZIkeF2M_-FHNzNGhLOw \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/rbac_rules.json b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..ef11e18f6 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-client/src/test/resources/rbac_rules.json @@ -0,0 +1,155 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE", "EXECUTE"], + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": ["specificSubmodelId", "testSMId1", "testSMId2"], + "submodelElementIdShortPaths": ["testSMEIdShortPath1","smc2.specificSubmodelElementIdShort","testSMEIdShortPath2"] + } + }, + { + "role": "basyx-sme-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-sme-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2" + } + }, + { + "role": "basyx-sme-updater-three", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc1.specificSubmodelElementIdShort-2" + } + }, + { + "role": "basyx-file-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor-two", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "square" + } + }, + { + "role": "basyx-file-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + } +] \ No newline at end of file diff --git a/ci/.env b/ci/.env new file mode 100644 index 000000000..5e316bdf3 --- /dev/null +++ b/ci/.env @@ -0,0 +1,3 @@ +BASYX_VERSION=2.0.0-SNAPSHOT +AAS_WEBUI_VERSION=v2-240125 +KEYCLOAK_VERSION=24.0.4 \ No newline at end of file diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index 4cc177c8f..ff216c98d 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -75,7 +75,7 @@ services: - basyx-java-server-sdk aas-registry-log-mem: - image: eclipsebasyx/aas-registry-log-mem:2.0.0-SNAPSHOT + image: eclipsebasyx/aas-registry-log-mem:$BASYX_VERSION container_name: aas-registry-log-mem ports: - "8050:8080" @@ -86,7 +86,7 @@ services: - basyx-java-server-sdk sm-registry-log-mem: - image: eclipsebasyx/submodel-registry-log-mem:2.0.0-SNAPSHOT + image: eclipsebasyx/submodel-registry-log-mem:$BASYX_VERSION container_name: sm-registry-log-mem environment: SERVER_SERVLET_CONTEXT_PATH: / @@ -96,11 +96,51 @@ services: networks: - basyx-java-server-sdk + secured-aas-registry-log-mem: + image: eclipsebasyx/aas-registry-log-mem:$BASYX_VERSION + container_name: secured-aas-registry-log-mem + ports: + - "8051:8080" + environment: + SERVER_SERVLET_CONTEXT_PATH: / + BASYX_CORS_ALLOWED_ORIGINS: '*' + BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD + BASYX_FEATURE_AUTHORIZATION_ENABLED: true + BASYX_FEATURE_AUTHORIZATION_TYPE: rbac + BASYX_FEATURE_AUTHORIZATION_JWTBEARERTOKENPROVIDER: keycloak + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/BaSyx + BASYX_FEATURE_AUTHORIZATION_RBAC_FILE: file:/rbac/rbac_rules.json + volumes: + - ./keycloak/rules/rbac_rules-aas-registry.json:/rbac/rbac_rules.json:ro + restart: always + networks: + - basyx-java-server-sdk + + secured-sm-registry-log-mem: + image: eclipsebasyx/submodel-registry-log-mem:$BASYX_VERSION + container_name: secured-sm-registry-log-mem + environment: + SERVER_SERVLET_CONTEXT_PATH: / + BASYX_CORS_ALLOWED_ORIGINS: '*' + BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD + BASYX_FEATURE_AUTHORIZATION_ENABLED: true + BASYX_FEATURE_AUTHORIZATION_TYPE: rbac + BASYX_FEATURE_AUTHORIZATION_JWTBEARERTOKENPROVIDER: keycloak + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/BaSyx + BASYX_FEATURE_AUTHORIZATION_RBAC_FILE: file:/rbac/rbac_rules.json + volumes: + - ./keycloak/rules/rbac_rules-sm-registry.json:/rbac/rbac_rules.json:ro + ports: + - "8061:8080" + restart: always + networks: + - basyx-java-server-sdk + keycloak: build: - context: ./../examples/BaSyxSecured/keycloak + context: ./keycloak volumes: - - ./:/opt/jboss/keycloak/imports + - ./keycloak/realm:/opt/jboss/keycloak/imports ports: - 9096:8080 environment: @@ -109,6 +149,25 @@ services: networks: - basyx-java-server-sdk + keycloak-fixed-uri: + image: eclipsebasyx/keycloak:0.0.1 + build: + context: ./keycloak + dockerfile: Dockerfile.keycloak + container_name: keycloak-fixed-uri + environment: + KC_HOSTNAME: localhost + KC_SPI_INITIALIZER_ISSUER_BASE_URI: http://keycloak:8080 + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: keycloak-admin + command: ["start-dev", "--import-realm"] + ports: + - 9097:8080 + volumes: + - ./keycloak/realm:/opt/keycloak/data/import:ro + networks: + - basyx-java-server-sdk + networks: basyx-java-server-sdk: name: basyx-java-server-sdk diff --git a/ci/keycloak/Dockerfile b/ci/keycloak/Dockerfile new file mode 100644 index 000000000..54bc49c28 --- /dev/null +++ b/ci/keycloak/Dockerfile @@ -0,0 +1,14 @@ +FROM quay.io/keycloak/keycloak:22.0.0 + +# Make the realm configuration available for import +COPY /realm/BaSyx-realm.json /opt/keycloak_import/ + +# Import the realm and user +RUN /opt/keycloak/bin/kc.sh import --file /opt/keycloak_import/BaSyx-realm.json + +# The Keycloak server is configured to listen on port 8080 +EXPOSE 8080 +EXPOSE 8443 + +# Import the realm on start-up +CMD ["start-dev"] \ No newline at end of file diff --git a/ci/keycloak/Dockerfile.keycloak b/ci/keycloak/Dockerfile.keycloak new file mode 100644 index 000000000..f7f0af270 --- /dev/null +++ b/ci/keycloak/Dockerfile.keycloak @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 +FROM maven:3-eclipse-temurin-17-alpine as build +WORKDIR /workspace +COPY ./initializer/pom.xml /workspace/pom.xml +COPY ./initializer/src /workspace/src +COPY ./realm/BaSyx-realm.json /workspace/BaSyx-realm.json +RUN mvn install + +FROM keycloak/keycloak:24.0.4 +COPY --from=build /workspace/target/org.eclipse.digitaltwin.basyx.v3.clients-keycloak-issuer-initializer.jar /opt/keycloak/providers/issuer-initializer.jar +COPY --from=build /workspace/BaSyx-realm.json /opt/keycloak/data/import/BaSyx-realm.json + +RUN /opt/keycloak/bin/kc.sh import --file /opt/keycloak/data/import/BaSyx-realm.json \ No newline at end of file diff --git a/ci/keycloak/initializer/pom.xml b/ci/keycloak/initializer/pom.xml new file mode 100644 index 000000000..62d5c64b2 --- /dev/null +++ b/ci/keycloak/initializer/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + org.eclipse.digitaltwin.basyx.v3.clients + keycloak-issuer-initializer + 1.0-SNAPSHOT + + + 24.0.3 + UTF-8 + 1.14.11 + 2.0.13 + + + + + org.keycloak + keycloak-core + ${keycloak.version} + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + + + org.keycloak + keycloak-services + ${keycloak.version} + + + + com.google.auto.service + auto-service + + 1.1.1 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + net.bytebuddy + byte-buddy + 1.14.15 + + + net.bytebuddy + byte-buddy-agent + 1.14.15 + + + com.cronutils + cron-utils + 9.2.1 + + + org.slf4j + slf4j-reload4j + ${slf4j.version} + + + + + ${project.groupId}-${project.artifactId} + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + + org.keycloak:* + + **/* + + + + io.smallrye.common:smallrye-common-annotation + + **/* + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerProviderFactory.java b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerProviderFactory.java new file mode 100644 index 000000000..ccb4be066 --- /dev/null +++ b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerProviderFactory.java @@ -0,0 +1,22 @@ +package org.eclipse.digitaltwin.basyx.keycloak.initializer; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; + +public interface InitializerProviderFactory extends ProviderFactory, Provider { + + @Override + default Provider create(KeycloakSession session) { + return null; + } + + @Override + default void init(Config.Scope config) { + } + + @Override + default void close() { + } +} diff --git a/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerSpi.java b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerSpi.java new file mode 100644 index 000000000..67da7d22c --- /dev/null +++ b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/InitializerSpi.java @@ -0,0 +1,29 @@ +package org.eclipse.digitaltwin.basyx.keycloak.initializer; + +import com.google.auto.service.AutoService; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +@AutoService(Spi.class) +public class InitializerSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "initializer"; + } + + @Override + public Class getProviderClass() { + return Provider.class; + } + + @Override + public Class> getProviderFactoryClass() { + return InitializerProviderFactory.class; + } +} diff --git a/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/realm/IssuerInitializerProvider.java b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/realm/IssuerInitializerProvider.java new file mode 100644 index 000000000..8ad4f8edf --- /dev/null +++ b/ci/keycloak/initializer/src/main/java/org/eclipse/digitaltwin/basyx/keycloak/initializer/realm/IssuerInitializerProvider.java @@ -0,0 +1,83 @@ +package org.eclipse.digitaltwin.basyx.keycloak.initializer.realm; + +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.eclipse.digitaltwin.basyx.keycloak.initializer.InitializerProviderFactory; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.services.Urls; +import org.keycloak.services.validation.Validation; + +import com.google.auto.service.AutoService; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; + +@AutoService(InitializerProviderFactory.class) +public class IssuerInitializerProvider implements InitializerProviderFactory { + + public static final String PROVIDER_ID = "issuer"; + + private static final String CONFIG_ATTR_BASE_URI = "base-uri"; + + private static String issuerBaseUri; + + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(IssuerInitializerProvider.class); + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public void init(Config.Scope config) { + issuerBaseUri = config.get(CONFIG_ATTR_BASE_URI); + if (!Validation.isBlank(issuerBaseUri)) { + log.info("Issuer BaseURI fixed value: {}", issuerBaseUri); + } else { + log.info("Issuer BaseURI is blank"); + } + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + ByteBuddyAgent.install(); + new ByteBuddy() + .redefine(Urls.class) + .method(named("realmIssuer").and(isDeclaredBy(Urls.class).and(returns(String.class)))) + .intercept(MethodDelegation.to(this.getClass())) + .make() + .load(Urls.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); + } + + public static String realmIssuer(URI baseUri, String realmName) { + try { + baseUri = new URI(issuerBaseUri); + } catch (URISyntaxException | NullPointerException ignored) { + } + return Urls.realmBase(baseUri).path("{realm}").build(realmName).toString(); + } + + @Override + public List getConfigMetadata() { + return ProviderConfigurationBuilder.create() + .property() + .name(CONFIG_ATTR_BASE_URI) + .type(ProviderConfigProperty.STRING_TYPE) + .helpText("The baseUri to use for the issuer of this server. Keep empty, if the regular hostname settings should be used.") + .add() + .build(); + } +} + + diff --git a/ci/keycloak/initializer/src/main/resources/log4j.properties b/ci/keycloak/initializer/src/main/resources/log4j.properties new file mode 100644 index 000000000..05fb2da9e --- /dev/null +++ b/ci/keycloak/initializer/src/main/resources/log4j.properties @@ -0,0 +1,8 @@ +# Root Logger +log4j.rootLogger=INFO, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/ci/keycloak/realm/BaSyx-realm.json b/ci/keycloak/realm/BaSyx-realm.json new file mode 100644 index 000000000..36ee74513 --- /dev/null +++ b/ci/keycloak/realm/BaSyx-realm.json @@ -0,0 +1,2958 @@ +{ + "id" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "realm" : "BaSyx", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "efe8c80d-bcd5-4a3c-91a0-a397a80d1d52", + "name" : "basyx-updater-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "9b70ce9b-1b39-4f5a-893d-9f8956cf5dad", + "name" : "basyx-reader-serialization-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "4bbc59ce-901e-49b9-adeb-0511469595df", + "name" : "basyx-aas-discoverer", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "52e7db01-dd27-4589-a530-ec8491bd2026", + "name" : "basyx-assetid-deleter", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "20c8f106-d2fb-422d-9045-22b28151f792", + "name" : "basyx-sme-reader-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "14dd6864-bcbd-46c3-b9b6-269ce036badc", + "name" : "basyx-uploader-three", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "502bc902-9de6-4552-98b0-55187b847272", + "name" : "user", + "description" : "", + "composite" : true, + "composites" : { + "client" : { + "basyx-client-api" : [ "basyx-user" ] + } + }, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "d3323aef-0e1f-4ec0-ba54-e0b3f9a897eb", + "name" : "basyx-assetid-discoverer", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "43a09e41-bcfb-429b-8675-eaf116ad4f1f", + "name" : "basyx-updater", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "4028f02d-3ee1-4c18-9b6a-a22c8bda51de", + "name" : "basyx-sme-updater", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "6ce3248b-7c14-42b4-9cbc-e1237851d778", + "name" : "basyx-creator", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "5702a4dd-4ccb-44b1-805d-fd9b1c333492", + "name" : "basyx-sme-updater-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "5ad9c765-2075-4cc4-b41e-c1b11cd544c4", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "797d2956-a895-4171-ab44-2fc9dbcf7f4c", + "name" : "default-roles-basyx", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "94394113-64a8-4cd1-9212-5a0cd955187b", + "name" : "basyx-asset-updater", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "f9df352e-269d-4a5d-a263-105d8ab3ae52", + "name" : "basyx-reader", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "469ec431-9a4f-4d87-80fe-cf2c7bbd5d37", + "name" : "basyx-reader-serialization", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "b70c22a9-e17e-4914-ae43-2752bafe356a", + "name" : "basyx-asset-updater-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "a537120f-8ccc-47a8-a1a7-9229d72561e5", + "name" : "READ", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "ebf827ce-862a-413b-afb3-5ad410ddf4ac", + "name" : "admin", + "description" : "", + "composite" : true, + "composites" : { + "client" : { + "basyx-client-api" : [ "basyx-admin" ] + } + }, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "32b591a9-55ed-4940-a7ad-efb3c40c3d38", + "name" : "basyx-executor-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "7b698a18-f272-4178-a6a2-d09e714c488e", + "name" : "basyx-uploader-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "09fa63ab-86ae-40bb-9497-56ee46070200", + "name" : "basyx-sme-reader", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "7065a5d2-3ab5-471a-be8c-cda64b6ce319", + "name" : "basyx-uploader", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "012af7ea-5eb7-4156-929a-acbae548e105", + "name" : "basyx-deleter", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "53212b19-655b-4e13-ad31-ec8c7d43d35d", + "name" : "basyx-deleter-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "2d2873a1-e636-46b2-bc89-5d8ca3fcde9e", + "name" : "basyx-executor", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "7a2111c1-7d1f-4b41-a0de-bfe314b73b72", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "05885c8f-e81f-47fa-bf47-c07153fc7b1b", + "name" : "basyx-file-sme-reader", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "5b5d0f1e-777f-4342-8128-b9eff69aed17", + "name" : "maintainer", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "afb72d5e-0841-452d-b3e0-5268dcba4c2a", + "name" : "visitor", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "b007c30e-c4bc-46ad-b72f-8ce67ec129fd", + "name" : "basyx-assetid-creator", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "50fb06f4-fe2d-46d8-b02c-5f5c409e4ce5", + "name" : "basyx-file-sme-updater", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "905eadf9-8b63-4503-9022-2f33daaa3372", + "name" : "basyx-reader-two", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + }, { + "id" : "a0dfe40a-8ec0-492c-a2c4-fa0ff9275918", + "name" : "basyx-sme-updater-three", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "1752f599-6520-4588-9a85-75049a5f4ea7", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "33fd6cb3-2c41-4d41-87c7-56dece25892c", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "c083167e-2b27-4860-9117-07d01eaf9d28", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "88458e07-82e7-4e8e-a262-4c2e271dfe9c", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "940a767e-8370-4ef4-aec4-c616393b3ff5", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "33c113a8-2aa6-4f0a-a0fe-80f6c74e691d", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "8db1ccc7-4484-4d98-a32e-1125487bbfa7", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "70a4bece-8b1e-4f3e-86b9-56f56851880f", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "f4acf86a-1877-4c8d-ab97-d30941ab1952", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "46693e69-db27-4e94-a4a4-8e4e14cc3cd4", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "206be815-72c8-40ab-a4cd-7020d1f72942", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "e1cf18c5-6635-4f1d-8efa-d86c609515d8", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "3ec28e2e-89f8-46af-8655-fde414a9bd28", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "44e2e3d4-1501-4490-adbf-5376e0d8ce17", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "add14c5f-311c-42f3-9595-2fe7c36d6c2b", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "7ba6fa14-a7a0-4072-9a7c-247c5df3e60f", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients", "create-client", "view-clients", "manage-identity-providers", "manage-realm", "manage-authorization", "view-realm", "query-users", "impersonation", "view-authorization", "manage-events", "query-groups", "manage-users", "query-realms", "view-events", "view-identity-providers", "manage-clients", "view-users" ] + } + }, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "978756e4-01e3-4fee-b0aa-b291e7a4d8d8", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "0ccfa96c-7e90-43b3-ba58-cb70b42456ca", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + }, { + "id" : "440c9f2a-5b40-4971-a9e6-044a118561ba", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "attributes" : { } + } ], + "basyx-client-api" : [ { + "id" : "2dd4b9b1-748f-43f3-b62b-048c92ae79d1", + "name" : "basyx-creator", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "attributes" : { } + }, { + "id" : "ba077409-1b5d-4fc8-b20e-10389507fb75", + "name" : "basyx-admin", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "attributes" : { } + }, { + "id" : "05ca5b90-4eda-4a58-a724-bfc61d1c4a05", + "name" : "basyx-user", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "30909060-d910-4a45-8bcc-059768731492", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "45f21c3d-4e85-466f-984f-d7bd47392453", + "attributes" : { } + } ], + "account" : [ { + "id" : "5b81c2c9-2460-4b8a-abd8-a685292eb7ce", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "74daac96-8775-4666-8e13-070049c6d8e7", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "d9580b03-2736-46cc-97ab-d2f62301df1d", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "bf835a6b-c6f6-47a2-9e2b-c082cbba801c", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "d862864d-24af-4dac-b35d-b27e4c5bd081", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "7ccd9a2b-458b-4981-ad5f-543701dbace0", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "7e16ae1b-8db3-44ea-8bfd-879e6d8ac53c", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + }, { + "id" : "8c63a071-2ca5-4991-8a37-bccf7ef696b0", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", + "attributes" : { } + } ], + "basyx-demo" : [ ], + "workstation-1" : [ { + "id" : "914a18c6-4f14-418f-99e0-bfdcf604ac01", + "name" : "uma_protection", + "composite" : false, + "clientRole" : true, + "containerId" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "attributes" : { } + } ] + } + }, + "groups" : [ { + "id" : "606a14f2-6114-4fd3-9ca6-4a53514fffb9", + "name" : "BaSyxGroup", + "path" : "/BaSyxGroup", + "attributes" : { }, + "realmRoles" : [ "basyx-deleter", "basyx-creator", "basyx-asset-updater" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], + "defaultRole" : { + "id" : "797d2956-a895-4171-ab44-2fc9dbcf7f4c", + "name" : "default-roles-basyx", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "bcb69552-bf11-4249-a3eb-d0c3ab54a570" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "856b093b-ef9f-4bd0-92ca-662f680c73cc", + "createdTimestamp" : 1713968958846, + "username" : "basyx.aas.discoverer", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "bc4944be-c8e2-4c91-81cc-9478a0795906", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1713968969072, + "secretData" : "{\"value\":\"V1xFTsjw4G4nJY+ftBt4CavQs1d8zf0ybVtUhxQR0yg=\",\"salt\":\"XjYZbMDK9iyDdsZe2WXb6A==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-aas-discoverer", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "aef2b331-f694-4503-b0da-1412c77842ba", + "createdTimestamp" : 1702179260496, + "username" : "basyx.asset.updater", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "24db17af-1244-4cea-8af8-1047adcd0753", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702179278395, + "secretData" : "{\"value\":\"YRTadONVBjE5bQjGmz2mtnr3X4IlU+4xxbmq2aBRuIo=\",\"salt\":\"wfqiItiwdM2FBYJPENkQ2A==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx", "basyx-asset-updater" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "4fc75aa9-4745-4bec-846e-de5dbd665b7c", + "createdTimestamp" : 1702179349503, + "username" : "basyx.asset.updater.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "db64f531-b8ab-4e33-b7b9-e3ba0e3677d4", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702179363013, + "secretData" : "{\"value\":\"6HRJw+wSlbSrf8wcAGjnn5/PnD8ARlZOnJK7p87+VOc=\",\"salt\":\"VDu0YOIlhG3WHofMj2hSeg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-asset-updater-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "bcbd95b2-2ec4-42a6-9b1d-39dabf0454c3", + "createdTimestamp" : 1713969008326, + "username" : "basyx.assetid.creator", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "81b26fa2-4eda-45d7-a8be-4b6d64b3d813", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1713971859329, + "secretData" : "{\"value\":\"yNEw5ey4rT1a0dONFUpVe7YaNfIrobNkCdL6DZjBj1A=\",\"salt\":\"JLMeQM7gjHenb5JhfzhWQA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-assetid-creator", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "2def798c-7547-42fe-8915-be493d740005", + "createdTimestamp" : 1713969038565, + "username" : "basyx.assetid.deleter", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "fa285b2c-9a2d-4400-a524-508b0a16f4c3", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1713969048116, + "secretData" : "{\"value\":\"28gYVMfDZofypgu67nmV5Kv2KrPGEFshT5tYvj+LPf8=\",\"salt\":\"ZZI9X8eR7pl7YqmgC06FYw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-assetid-deleter", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3527f121-116c-429b-bf0d-78bf4a8b5abe", + "createdTimestamp" : 1713968905507, + "username" : "basyx.assetid.discoverer", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "4f3427f8-9da2-4372-a54b-c1e54dc8da68", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1713968917922, + "secretData" : "{\"value\":\"uUOChDW0wpWybeiWSqsyqKSfLN0CWANylCjzNavInB4=\",\"salt\":\"TsyGowE4zc8gsp4wRREOAQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-assetid-discoverer", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "f48119f5-7dff-46af-b9db-3ee96cd52550", + "createdTimestamp" : 1702032555719, + "username" : "basyx.creator", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "BaSyx", + "lastName" : "creator", + "credentials" : [ { + "id" : "393aa50c-70ed-4676-b5d0-b4c6cf930272", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702032714401, + "secretData" : "{\"value\":\"/TOCJJp9SrRtZnB+QxjdzJCKONo5IDy/C/H5GyhsRm8=\",\"salt\":\"2hB/91M1PTQmtp9w8ULRMQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-creator", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "40858874-f6e7-48c8-9667-d585d7c27b57", + "createdTimestamp" : 1702032602188, + "username" : "basyx.deleter", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "BaSyx", + "lastName" : "Deleter", + "credentials" : [ { + "id" : "ee9224dd-6277-4c3d-8d88-b595d66f25c7", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702032741081, + "secretData" : "{\"value\":\"eQ18xAGwRazaHAdd6F+WnhIDKlFn9uTUvgrYOY8sfUg=\",\"salt\":\"0CwKlmVofsusehhhfJZExA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-deleter", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "fcf7ea95-875e-42ef-afe1-f9b6008cbaf9", + "createdTimestamp" : 1702161686852, + "username" : "basyx.deleter.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "1b8d0b8c-1f65-43e0-9178-785533746684", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702161714301, + "secretData" : "{\"value\":\"/iZkSX4POJyi8GuL3+fMGlPVcLkssWMTA5KWt48IeXc=\",\"salt\":\"/5sjiGBNXC4xzAHjWFJr+g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-deleter-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "8e820205-13fa-4d61-9513-99f717c15f73", + "createdTimestamp" : 1705326903328, + "username" : "basyx.executor", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "3e891de4-74e6-46cb-840f-6681af0ce397", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705326918687, + "secretData" : "{\"value\":\"ytSpEUoTvStjF057boJpFgx59agjUC9pJiM/mdrtc7Y=\",\"salt\":\"U30NyBIzFGYhoqC+DCuMZQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-executor", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3d645000-277e-46f5-b421-85f1fdb064b5", + "createdTimestamp" : 1705326932748, + "username" : "basyx.executor.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "d21c920f-5dd0-4ce7-ad2e-737a841bb1f8", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705326945517, + "secretData" : "{\"value\":\"uAi1tN7YY/byDS0vay77q3nlJsb9gkC5lGiB8SROcIw=\",\"salt\":\"/zhdEnnDDFvuWMT9ZzLMSA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-executor-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "c86047c6-3ab8-4f47-86ba-b26ea80d1986", + "createdTimestamp" : 1705398014689, + "username" : "basyx.file.sme.reader", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "bd374dc3-4fec-4232-be91-a3f5608e373e", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705398026388, + "secretData" : "{\"value\":\"3p7BdFuY8JBRDaoD0Lv2i3N2XCvuM7mt5tUhdyJ+sRA=\",\"salt\":\"3+GFcXL0EAlfjmP3CwtpLg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-file-sme-reader", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "da7acf36-1574-4c0a-aa5b-65829a512b60", + "createdTimestamp" : 1705398962710, + "username" : "basyx.file.sme.updater", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "e5fda50d-9f52-4f60-a917-31a85fc275a7", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705398974508, + "secretData" : "{\"value\":\"sQsBVFdIJj7whmjzlFHkrh+ZCtrj8oXaOuK4V1q+95I=\",\"salt\":\"G1xU/pxUi5Moj3V7dRhfOg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-file-sme-updater", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "abfaa545-3c2f-4ac6-9f41-7c166613ea35", + "createdTimestamp" : 1702032528855, + "username" : "basyx.reader", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "BaSyx", + "lastName" : "Reader", + "credentials" : [ { + "id" : "eea587f5-439d-487d-85d0-587b226f0683", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702032760779, + "secretData" : "{\"value\":\"fNd3UFdT3clPTDFTMgdFzpNN6R34wu0R23S2vV6fOgI=\",\"salt\":\"eMifD6Sp0urGRzhEhSDc4w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader", "default-roles-basyx" ], + "clientRoles" : { + "basyx-client-api" : [ "basyx-user" ] + }, + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "37e1ba01-3b9c-428c-b0fe-be85970bf1d9", + "createdTimestamp" : 1702158534562, + "username" : "basyx.reader.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "3237f89d-f9f0-4b70-9a08-d31d995c6948", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702158617052, + "secretData" : "{\"value\":\"5aTlmhGaKIHjdeAWoC6+ei2WGOBE62okIiVm6h0/Ur4=\",\"salt\":\"qkVSxkETPCSxTqAERBjcDw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "2d7a7bdb-c8d3-4684-8c06-8873aabd9968", + "createdTimestamp" : 1707983800892, + "username" : "basyx.reader.serialization", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "c35c2724-8ae9-4193-bd25-e5245df77f4d", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1707983812752, + "secretData" : "{\"value\":\"2PxseHKNw94KIO+vkxn+jLwrfRWFF3Eh6cTOviSyKTI=\",\"salt\":\"E8GDPR/T+enGFszv/XvlQA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader-serialization", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "113691d0-fc03-4607-8b64-97b646bded1f", + "createdTimestamp" : 1707983824403, + "username" : "basyx.reader.serialization.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "86bd4fdc-077c-4546-b80a-f51788f15b56", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1707983834216, + "secretData" : "{\"value\":\"LMmwk+0uIx2VOOD/Llx19E6oUW6Z+fye0dD6fft8JlI=\",\"salt\":\"mWFD3gAs2jncjP6tBu9C3g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader-serialization-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3988486a-51eb-447b-bc87-354e5b724c76", + "createdTimestamp" : 1705306854299, + "username" : "basyx.sme.reader", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "d70787d1-9834-43d6-a10d-bca854847fec", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705306868293, + "secretData" : "{\"value\":\"DaO6udzDHUKnoqpl/7bdprLMKfwqV7MjKme1/NnEQtA=\",\"salt\":\"SzPSSVEQd1ZZoOGLuWGInA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx", "basyx-sme-reader" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "ed921577-8b07-439e-af66-cc3579a276ec", + "createdTimestamp" : 1705310445991, + "username" : "basyx.sme.reader.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "62446675-7b7e-40e7-bf6c-b375d8542a12", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705310456309, + "secretData" : "{\"value\":\"QQkWFYjV952eOqGdkcDqqF4OdgtMCmUSKxzPF5I1LrU=\",\"salt\":\"ZUxajrFJ/cFxxH28cz8ibQ==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-sme-reader-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "60f02977-d033-43ed-8171-8ac44d14c62f", + "createdTimestamp" : 1705312271325, + "username" : "basyx.sme.updater", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "d224c0bc-7c5d-426d-8e96-9f4dfb141eab", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705312282450, + "secretData" : "{\"value\":\"q+x0JQZAGRyDLlVAWPzLmVCJ/PPpb07xW+bAv1QFw9s=\",\"salt\":\"QC5VcH0pvVpnVPC/EnTAuA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-sme-updater", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "9fd79b98-a230-43c8-a7ff-f88ee381c58c", + "createdTimestamp" : 1705315980352, + "username" : "basyx.sme.updater.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "eca8536c-fbf7-49dd-a370-405b00336965", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705315990230, + "secretData" : "{\"value\":\"KpVPW6pBjilR3xUTV5X/Y8g2javOsmkY7/9TLwix8Lk=\",\"salt\":\"xmj+BOrdA7DQks9wwxV42w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-sme-updater-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "20336dcf-0c1f-4344-b31b-f26048fc7faa", + "createdTimestamp" : 1705324670982, + "username" : "basyx.sme.updater.3", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "d1652462-350a-42e0-9a3e-e04cfa258237", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1705324695639, + "secretData" : "{\"value\":\"/qEHkHQIlm0poWXk13LWf3TpXq/ffOzflkIbretgeBw=\",\"salt\":\"arEuG45sRa+2aOZiY9PqVg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-sme-updater-three", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "44f291ad-a0de-4035-9938-a092faf810b5", + "createdTimestamp" : 1702032579778, + "username" : "basyx.updater", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "BaSyx", + "lastName" : "Updater", + "credentials" : [ { + "id" : "28e62a3a-790d-477f-b139-b0ea943abfd7", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702032778389, + "secretData" : "{\"value\":\"ZfuHUQ78g18C9k/zjHn/QBKsD8e9+xhxomRaBI00oSQ=\",\"salt\":\"IIhszlFoc5V3AKG0sDowkg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-updater", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "9bf6b5c8-194f-4672-9313-9f3823cb3019", + "createdTimestamp" : 1702161564532, + "username" : "basyx.updater.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "59ef29d6-6dcf-42e6-b946-33d50335e0d1", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702161581051, + "secretData" : "{\"value\":\"IWJZlX5XJT4IoSH1npG+APct5iL931bC54+RtjRgKAw=\",\"salt\":\"YZW26Yurco6D2uhzjVcQjA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-updater-two", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "cb7df854-827d-4d34-a01f-33cdf07f5cea", + "createdTimestamp" : 1708702290219, + "username" : "basyx.uploader", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "7df6170e-1631-434d-8380-62f750c563cf", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1708702304670, + "secretData" : "{\"value\":\"M7YGdklaJzphwjjWGLfb990lR4NY4rbLTQ1LAPptEuc=\",\"salt\":\"MEYVvEIkg54+jjXQS47dbA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx", "basyx-uploader" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3c94e1e0-caac-48c3-a31c-c9f555233a46", + "createdTimestamp" : 1708932762171, + "username" : "basyx.uploader.2", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "0d4049cb-293b-4f76-b82b-5ba9ee4530f2", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1708932779332, + "secretData" : "{\"value\":\"jNTZeVbTOOtuokGeXGhYd5Aa+G9TkCS1RFikWtULN/w=\",\"salt\":\"9St8VUxP3iiO2jZYM7whww==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx", "basyx-uploader-two" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "c1a592bd-536f-4a3e-8193-c17d479814f3", + "createdTimestamp" : 1708934121191, + "username" : "basyx.uploader.3", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "7cd8fd3f-8c83-43b9-9fb7-6203e7c7376c", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1708934138452, + "secretData" : "{\"value\":\"BenBZDPAZWMjSh21uER8mw7PSEdXw8xRh7YPlWsNUno=\",\"salt\":\"ioGE5CqXT8XyCWkcbi9ETA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-uploader-three", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "f3ec1793-3d62-41c3-ad34-b7b29ac88528", + "createdTimestamp" : 1702030619322, + "username" : "bob.maintainer", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "Bob", + "lastName" : "", + "credentials" : [ { + "id" : "a45e5184-e7f3-41bc-be25-c97197852301", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702030635868, + "secretData" : "{\"value\":\"qMAW5hUeZYBsbUjNqSHcRFIn05OKnS/whcapkCvLL3c=\",\"salt\":\"BsbKzzXVRR6sazSpRimCeA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "maintainer", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "e75ac9e8-7093-4898-a203-d9839f854944", + "createdTimestamp" : 1702030567684, + "username" : "jane.doe", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "Jane", + "lastName" : "Doe", + "credentials" : [ { + "id" : "d1e97a9f-42b6-43d0-a070-1216d03a64b7", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702030585530, + "secretData" : "{\"value\":\"S/d8o0wllcaXTOdtd/DGSIY9K6irGF0eMn9QJxZ+FSk=\",\"salt\":\"jIVz5vxpb6RpVHxkqi7jSw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "user", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "caf7499e-4f3d-45fa-9246-99ea8f8b5c94", + "createdTimestamp" : 1701764678734, + "username" : "john", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "136b209f-3b75-45d9-b448-e1ec93dc7ea4", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1701764705215, + "secretData" : "{\"value\":\"wl3q7M/vsTL2T2vVXnZQG8eRzktKT5WRqDP+d1sW2tE=\",\"salt\":\"Yx270JeMPiFH36ycimpziA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "READ", "admin", "user", "default-roles-basyx" ], + "clientRoles" : { + "basyx-client-api" : [ "basyx-admin" ] + }, + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "fb833c1b-3a7d-4224-9ab1-672e7203bab5", + "createdTimestamp" : 1702030523698, + "username" : "john.doe", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "John", + "lastName" : "Doe", + "credentials" : [ { + "id" : "b8b3d5cf-4fa3-46ed-8a3e-18875acecff0", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702030540643, + "secretData" : "{\"value\":\"PAA5qfnQC9ImTZslJXw6GSW6k7rhYHl7XCdlk+9yb5A=\",\"salt\":\"jtFqUjRq7RtxXNYrKSJgFA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "admin", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "9045c192-428a-4fff-bf7a-b9a9ac16742f", + "createdTimestamp" : 1702030666980, + "username" : "paul.visitor", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "Paul", + "lastName" : "Visitor", + "credentials" : [ { + "id" : "81d4c206-c0a0-4ee6-9fcb-19f755e66150", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1702030682803, + "secretData" : "{\"value\":\"MedIFuT+Rl3PrjV7Nn/DJxJRiQD0Ucl6Ms2fc3wXbtU=\",\"salt\":\"12rxIShfyTRisjU8t7Smhw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "visitor", "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "a19abcac-34d5-46bb-a604-b07dc234e80f", + "createdTimestamp" : 1715582034760, + "username" : "service-account-workstation-1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "workstation-1", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader", "basyx-deleter", "basyx-updater", "basyx-creator", "default-roles-basyx" ], + "clientRoles" : { + "workstation-1" : [ "uma_protection" ] + }, + "notBefore" : 0, + "groups" : [ "/BaSyxGroup" ] + }, { + "id" : "77957093-d593-44b4-b4e9-bc365e840cdd", + "createdTimestamp" : 1715539640949, + "username" : "test.user", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Test", + "lastName" : "User", + "email" : "test.user@gmail.com", + "credentials" : [ { + "id" : "11672ccf-38b7-421b-8d01-755e0f2197da", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1715539657887, + "secretData" : "{\"value\":\"7YbCeFyuYDpwu/06UbMT7OObo29RU7cG1XWrqcNwZLg=\",\"salt\":\"nAQ9KKDYyg5GGuSUyfG+kw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ "/BaSyxGroup" ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "049e1323-6efb-4543-bc52-566cd292732a", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/BaSyx/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/BaSyx/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7d00b9a7-d212-4132-91f0-06e0719c7b43", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/BaSyx/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/BaSyx/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "f5343ae7-2d59-45ff-8c56-7fcc152d90f5", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "00ebf255-9c7c-444d-92b5-ff146d9147b9", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "clientId" : "basyx-client-api", + "name" : "BaSyx Client Api", + "description" : "", + "rootUrl" : "http://localhost:8081", + "adminUrl" : "http://localhost:8081", + "baseUrl" : "http://localhost:8081", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "http://localhost:8081/*" ], + "webOrigins" : [ "http://localhost:8081" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "http://localhost:8081", + "oauth2.device.authorization.grant.enabled" : "false", + "display.on.consent.screen" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "615f29de-5f3d-4384-8b71-7351ff2c2a32", + "clientId" : "basyx-demo", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "41f9RznNmtHaWKT7i8IDYTjl2VaLHw0q", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1716897911", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "45f21c3d-4e85-466f-984f-d7bd47392453", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "205e3c12-0af6-4d19-8eb4-d660d854ee43", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "2f43139f-46dc-442a-84b6-1eef146c0b16", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/BaSyx/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/BaSyx/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "70e02310-8910-4ad2-b02e-3091643fca8c", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "clientId" : "workstation-1", + "name" : "Workstation 1", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "nY0mjyECF60DGzNmQUjL81XurSl8etom", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "authorizationServicesEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1715582034", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "1f332577-69b1-48cf-b2d1-95ccd6159fdf", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "05688508-b82b-46c5-85de-f0bacf03d6e7", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + }, { + "id" : "a1c32e51-4369-4460-a015-98867bb101fd", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "e0f355da-f9ff-4104-b305-043b0188747b", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "a194eeae-0c0b-4300-b613-9c7b5a281ba0", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "c3c7617c-2d46-4cf7-893c-776b7a14797a", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "69675a1a-f2a5-4316-915b-e1a0cc02e0fe", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "e9883191-5f1c-4eed-90da-51806ba954cc", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "95a35d30-fb91-4a8c-a208-5c7b1644b7fa", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "51873e8d-85db-4c1f-be02-bef88d435e89", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "d937e76f-71f3-4260-bbc6-1feffc3a655e", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "gui.order" : "", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "7dfd8525-9c12-49a1-a6ec-4d4d1bb74471", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "false", + "user.attribute" : "foo", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String" + } + }, { + "id" : "2fe9cc2c-3f61-446e-9cf4-f34fe1964a1d", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "false", + "user.attribute" : "foo", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String" + } + }, { + "id" : "4071dcc6-b7d3-42b1-93c7-e14d0a17d103", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "c22bdca5-b67a-4249-a8c7-9bbe8fc16559", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "d62a405a-d99e-4d6a-bed4-48c052abc559", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "531eab62-3750-4ed9-b101-9f1c2e709c51", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "9ddb9d40-7d9e-48de-8069-dd4e49e781dd", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "ae918ce8-12e6-4cb3-be0d-243cbf083fcb", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "59542d0c-b9b4-4913-ba99-416cf5c4a725", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "2359cb5f-9de5-410c-9e87-38f1a4db0eee", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "dc0f74c2-135c-4e61-b74e-22836524b496", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "a715f65e-3a94-4bca-94df-49e1e53e5aae", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "11a8d21b-ed5d-4567-af88-91101e132553", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "c6ae1222-2561-491e-8c33-2e2eef183f6a", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "aba683c1-ee38-4f0a-980b-9c7c31a189ad", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "b5d111f6-91be-4735-9dbf-848bcf86c1d3", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "8e0ca67f-030e-4ea9-af3c-8e7815442957", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "3d9431ca-a590-457b-a0e3-412f63f07923", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "b54a324a-b8ee-4c66-b780-50f9b8e3e275", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "6ffd147e-fac7-4299-ab9f-b7bef677d0ae", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "9637b112-a2e8-4787-be6d-45f8d0804555", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "36e9530e-da81-477e-907e-a335efff8df8", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "119fb217-5662-40db-a33c-442846a58b71", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "b607712e-db7c-42d3-ab9c-99acf62bac40", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "c73bc111-8b12-46ac-866f-1e690c8fa21a", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "8de0aa23-d84c-4f77-9ada-52d6bcd71593", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "85b4a69b-2b52-44e0-b4f7-4ff75feba1b4", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "691dbb7d-ed16-4283-b737-f02676e56e82", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "46d9b7a5-0776-496d-9a50-e5584709677b", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "web-origins", "acr", "roles" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "f44d4d8f-cc39-4467-bb75-889f8d4c9b90", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper" ] + } + }, { + "id" : "7256d195-1e91-4f63-a9c4-6bef95243a92", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "f3d9ee71-6796-41bb-b89f-c4b2ad108b3a", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "340f74d5-41a0-45cc-8ccb-65a0a4c49ed4", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "fb2bea0a-dca5-4784-822d-cf10518f41c6", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] + } + }, { + "id" : "face2c9e-4d23-44e2-9a09-74e1d8448bd3", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "6d38ba87-78ee-4ca3-aecd-0164922a08e2", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "d9bdd722-325e-41ff-bb88-df8772c9415b", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "9948a63f-b171-4137-bb81-beabd0c049f0", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "59f8ad80-8417-45dd-8196-c2f37ddaa309" ], + "secret" : [ "D82qLVou0ux0UswcrMSTlw" ], + "priority" : [ "100" ] + } + }, { + "id" : "4a3be057-744a-44c4-9211-9a98d7c6303c", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEogIBAAKCAQEA9KETJD3hVFueAg6Dk39nNIhTNDGtEbOfz15JumFd23CNfceSgZB2fQn6/kwzxom+i2Za8NrcJh21sDbeXb0VQNnOWsjiOzDuC6VvRFzeg3eIr6BqQIEggHwk2YRqd/lD225Myk5s3G1PA/cLr0ZO+/IUwI9Z6rKayzSO0IrpHKe4cJUa6YGMeVeprN575/jcbbC+B8IXJdmcJ1+n5MUzHxpcvDFKLBOaukGTENji4qMoHxQ2YkGO5nOTDlq6Nx5J+VUMrUfP3Z7fJPvil+ma6jZtre4T3r7DoKv6jzQRbDUig/aFyx+qqStP+nPiRJ7sx6KzyJSekXPPY4bDkVdVXwIDAQABAoIBAAut4J6Tb72ANETpd+GwxiogmhK89NDXPRMaIs+WeqcrGMfZB+C0sQ60mi2a6lvmID+o7tpevWT6eFd5GL7r880chcWy0o48S0xccezKUq5/JGD4k5B2oCkfBDL4WsCImSZ4Sr7b5Xmh15E0y1nrM7X2orc0AN+u9quRKLWPJ3Fmy21NNOS6FVcAUwSJViDzG+heH8Ny91OnQs3SSh/0Ubxiv/bl54FVAoq1vrTl5xHBu8T+jXU978P6n8O8tXLaCEReAAcwrSAw98nNIuPOzqyZS5VLpxQCaVoIotorysPifrOQJFVHYK9az+jdkT1enxMdhmEewWW6KlQdUNeA+XUCgYEA+wzLLNAixryRgdlWvmmg8hVUAgXPczlDOfSOajSX7UcykTz/m0qjZ2idwUyMQvy+/ukqdJieSMegTWSq9kDyYW5THSI0GlIaX+2BMQml+enrGkDWp2Hl1G5EK9z98aw/fdVodn7U7XQKm3v1toF02Lx7sit6KFPCupg2lr248NMCgYEA+XPfFq2TuDCKhm9+yLQm5tyTzbDOfKqWAKYCYIO0iPMzeOwc09W0UOT6iW2WhFpus3jtp8lPmzfJ7MmIQ9RwhwporMGRPbV/tD7TEvCgOUOEsJv6eHgLqA1zCLI9kx1uezQglF1seb77ouOD6gTG/kK5HfwEd8GLV53TtYjYEcUCgYBerrOOAi2rgIDsVRjnFZVy1+JoJOLZlRYqrHZtzcoi3kBPEI9idSLtpEIjHgikVwh9wViWwtynnEp0BeyIlXQUlPRZv4WhC6gQ19VqtjXX7IYTz7JlTIHOvYuOc3l/BTSo86zDTBBoQeSiQX0pSOfVujh4uPcIcJa3oyKrdYBjqwKBgBfGKU7aACgfyDQD0EuEj+iUwSlrXmKXR3CMYdGc+8nJk/BEYIL37RWAnTgXz66Rh9dnAQ9qqkGa0Y3Vrzz3tDnKjitYz2TYNXGSQz+c20hZ3P2QABEXL9U2Yu5DPocU2QC4+RnqxXnc79KYaGwXRHfbGBV5fVuEgKk7C4BTqHQZAoGAUur0G3Qdu7PDFXDYYYuAk78ilVhLIp+r4nv61C8ydO4PKQPjRC+ab/UH0OvlXmGKAqeLj/cbohtOuprcHmaqIEwzxxXwQrdkBCWo7LxrV0/Ke731V5XM59disjA+F8aR78nZPBLHz5JLl0sagkKA8V3t9EwQMJ//M+ZbAqoT/GM=" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmTCCAYECBgGMOOsFujANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVCYVN5eDAeFw0yMzEyMDUwNzM3NTVaFw0zMzEyMDUwNzM5MzVaMBAxDjAMBgNVBAMMBUJhU3l4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9KETJD3hVFueAg6Dk39nNIhTNDGtEbOfz15JumFd23CNfceSgZB2fQn6/kwzxom+i2Za8NrcJh21sDbeXb0VQNnOWsjiOzDuC6VvRFzeg3eIr6BqQIEggHwk2YRqd/lD225Myk5s3G1PA/cLr0ZO+/IUwI9Z6rKayzSO0IrpHKe4cJUa6YGMeVeprN575/jcbbC+B8IXJdmcJ1+n5MUzHxpcvDFKLBOaukGTENji4qMoHxQ2YkGO5nOTDlq6Nx5J+VUMrUfP3Z7fJPvil+ma6jZtre4T3r7DoKv6jzQRbDUig/aFyx+qqStP+nPiRJ7sx6KzyJSekXPPY4bDkVdVXwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDtGhf0x8ll3OWG0ZPBeq75OtrxGsLeF+Ic0LPMjlZvhJ6YOj79NKuILhBfECGoOeyp2xhrWXZzrNtgl/1LFULYjeBMwmUt9Hlcj/iNSQslOl3bwUxKtVZfGfEDmC4uRqVnVC9NdQlJ8s1Wea675OrFBQevtqZMApkTvQHzSpfhK+bNGyMpBW6zYleUzvE8jDu9AtlLhjAw2/HWJHrOI2DwMN16tNth4vUlKMRfSgr/0kmxuQbqfd3H+kPgd78EgkGob7bCL27d7rRHnCBT79bWBushpTY9TnqQvfwQthGKFICEhsXOreXwRuxgJfG70Q60iwfXexknZFeaqmLPIY8k" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "f7fcd439-e566-4b8d-8078-f300b494f90a", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAsQ4RUvlZXLjQpLizIMht46ASwpwUoPpUDTmUhxF/VV+ezHTHbTAdv/5RA1GgCyTjAQe/Ih6dLByZrJFaroyvqgIJdMRCb0MwajI1US0/NwHtVvo5dea/+GKeHGRzvYZjxVlooR/1xmskfAM/NR/NaOMUhr/TNV7n7LXEb06L55DYnqdqrUnhXLewBq1lo54GsMqxN4hlkc4nJ2uYUtWEkV4SlMyYRjXBlylpuWFO0+/FsmaqSx7CZWjNWmqKlxnvUgRrT5Vh+9ZgCAOmGtLuFYOVWzqmVjWtyiJ1pYfWjwc86XeWBFefVY1lkoNNoSYKV4AZIkeF2M/+FHNzNGhLOwIDAQABAoIBABCuWR5+pJcySGIEjsfbalMETVAtgVoLS6j1UFOLZ/pEqILty64zVtI0ZTCRW6lBH+wEbVMLHFbAGRBjn24Lji9PwtFP/kxaQbD3qwNLZoXJtjE4IPrFMzf1Hp1hL3BfPX4l5tArYS4BAS535dgc927W2iO55e3E08f+9hNgjJjT6fpFvKqsGsHkPVEQBr0p1vZ1JkO3FrhCP4s5HTRSHoiaFxQb2yceN/mSmQHTcRJv6yxHAuhaKL44r7KEeI8fyANvXhx+BZhu9aX9Esb72poRFOyV5Wu1QoVFpKIUNhELTJ0NOjSTiqThDXKK0EOgkooN5dhyeNRcQ4qwufUF4n0CgYEA56YwPasUGWF7ZqiqfSgXOHrMb4E/T9Y194tVM4J94eEVqNeUrqcphLsBG1g9MAmMo6rXjYnmYyVZmrmaqeaSUKpibg2Bg0yfe8p/Z0UjwHebKXAYOQDUQazGgcACV4zHPhQFUzgJ/WS3X2EJUXb8M5OBlThD94xXlT0szrjKFLUCgYEAw6q3sQDEuGm6kVSrH35CflhRcEyGYzAfG5XXUGr1BuPqzsVeQMXm5fzIvFDpSqP0E1LS1LjiQQJuRVbcEGLmpG38hGKD5LgFxx2/leu8a9+i7v1yZ6DaQB+XXrtiCoOSab8q8AeU/5AJm9JQI2jr08u//xnIy1tUh3t9wjMxRi8CgYEAvmZnolh6pb3tOt0JfOO16lNss33tdwafxv78IeFw7HcgYW4IpGF7i8BVUY9+g3xl36StlYWyGu35L2a9DEcbHjhdvQ1W1X/mWk5/13cJwsnMfAvJrRjUXcLQSpdylVl97rVwBw25kE/3NOtCSHZfJ1lnminsG41784ubx7I1Tz0CgYAahJ6UnJgMrjeczq7Ke+AjI3EWGSj1dGYi5PHjcjt3DZibIWCewrOCY/oIm6aieQnxPH5aWhw/10Z+m0ED74N2sXlRr9BURSRzUfLPLL3CmPKKFUtWBBTQL/fh3N9ysVY8gq4dDcoBwNGsjppR0jxz3d9NgX8XIG+aVQA8O5fn/QKBgBVXVKXNeEiNPSoayzDXNuCU14DintvN4+jq2kL4zmOOc1s+NHdqAP/FZfBaQwl6SDZer3Tx9oXCZatZV9y8IZEC6GXqQO7H/RPbMDcuT8B6KHPqf0SZ0F7XYL9FGwjVQ5k1ouOvWbeNPxvHfxcs0nJTpou4gklF3yO98ATFEvv1" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIICmTCCAYECBgGMOOsFGDANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVCYVN5eDAeFw0yMzEyMDUwNzM3NTVaFw0zMzEyMDUwNzM5MzVaMBAxDjAMBgNVBAMMBUJhU3l4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQ4RUvlZXLjQpLizIMht46ASwpwUoPpUDTmUhxF/VV+ezHTHbTAdv/5RA1GgCyTjAQe/Ih6dLByZrJFaroyvqgIJdMRCb0MwajI1US0/NwHtVvo5dea/+GKeHGRzvYZjxVlooR/1xmskfAM/NR/NaOMUhr/TNV7n7LXEb06L55DYnqdqrUnhXLewBq1lo54GsMqxN4hlkc4nJ2uYUtWEkV4SlMyYRjXBlylpuWFO0+/FsmaqSx7CZWjNWmqKlxnvUgRrT5Vh+9ZgCAOmGtLuFYOVWzqmVjWtyiJ1pYfWjwc86XeWBFefVY1lkoNNoSYKV4AZIkeF2M/+FHNzNGhLOwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCd6bCxZ0JvQ9/Wh1iY17lDbmbIfcm/DapqN3Q8q3VLTwvsg4mwL0lKinTztGbh2KpSmRlLTFsvED63k8hIt09FLvbPy8n7U/DddaUhxS3/MMcQ2u+Rg5zXNhIPMdqyvXD9+R9ToXToP2BfD23f8HgGXJ2y5D3u48PZ96g2SlbAbNTnESABSB3r7bcHMemT+Ud/7auSA45lLoJWmlQjJ7gbFk1Dwrv/AdYTBFM+LWflei9MQUBqP95960VbFmX9sHIBxTqQ5GxB8JNdVPSEdfJe0xP2Bh4I0+yPGDvoadncQYySnBvlmdpKy60+UWWrH9R1ritc/F///hFB77hDu2l/" ], + "priority" : [ "100" ] + } + }, { + "id" : "c6b34d4a-f4f6-4864-8c39-86b0b6762bb7", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "6c9fd13c-0afe-46e8-83a6-3fb95ba32dcf" ], + "secret" : [ "f9qfZHpSvHgZ2ti_iZjMfWEM3DyJtlxEpNEzunKXI7qbqnLzwbsMFI4n_vF9YpM4r6mlARVIBnffHgjAwPZvnw" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "76ff1526-2405-40a7-9051-977dfba08add", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "64d21769-b1ff-4012-8021-b724df8e759f", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e07a9d36-3a69-431d-ae89-3ac51675ea16", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "db117f4e-e2f2-40b0-9d74-c3be8f6aacaf", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "7a3723a1-f43f-44b4-aa75-d6d7f3c60fac", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "04b87ec9-fbc0-434d-ae6c-3b144d4c4fb3", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "12d1fb6a-2691-4616-921f-537b2930763a", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "72dd5217-9d7a-4b9a-8e81-c98390a05938", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "652e9b64-0250-41aa-ab19-b6742aa2ee4f", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "2707b9b1-9a49-44b9-bbd3-4333d6b16a98", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "1aee26cc-cca5-4eef-8fb9-afa36240b518", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "09bb06f3-ff4d-4b65-b609-fea90f93044f", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "10fe11ec-61e9-49a6-85a8-2cd4c24da4bf", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "67281328-09bb-4140-9cdb-5ea1bfcf827b", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e4d8833f-6b90-47a1-9d73-4fbf3c41e910", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "41e56e9f-589e-4857-b28e-f6b8754ae400", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "412c0d35-ba19-4ee0-92de-4b00fd52e3fa", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "eede5156-ace5-41f3-b86d-441278f5b337", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "2caebb6c-1b1b-42ec-ac41-1f1dabd609ad", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "8ef60759-3395-4d42-ba53-390f72df09d5", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "22.0.0", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file diff --git a/ci/keycloak/rules/rbac_rules-aas-registry.json b/ci/keycloak/rules/rbac_rules-aas-registry.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/ci/keycloak/rules/rbac_rules-aas-registry.json @@ -0,0 +1,82 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "aas-registry", + "aasId": "dummyShellId_3" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "dummyShellId_3" + } + }, + { + "role": "basyx-asset-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-asset-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "specificAasId-2" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "specificAasId-2" + } + } +] \ No newline at end of file diff --git a/ci/keycloak/rules/rbac_rules-sm-registry.json b/ci/keycloak/rules/rbac_rules-sm-registry.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/ci/keycloak/rules/rbac_rules-sm-registry.json @@ -0,0 +1,82 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "dummySubmodelId_3" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "dummySubmodelId_3" + } + }, + { + "role": "basyx-asset-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-asset-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "specificSubmodelId-2" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "specificSubmodelId-2" + } + } +] \ No newline at end of file diff --git a/examples/BaSyxSecured/keycloak/BaSyx-realm.json b/examples/BaSyxSecured/keycloak/BaSyx-realm.json index c96d45aad..36ee74513 100644 --- a/examples/BaSyxSecured/keycloak/BaSyx-realm.json +++ b/examples/BaSyxSecured/keycloak/BaSyx-realm.json @@ -511,6 +511,14 @@ "attributes" : { } } ], "basyx-client-api" : [ { + "id" : "2dd4b9b1-748f-43f3-b62b-048c92ae79d1", + "name" : "basyx-creator", + "description" : "", + "composite" : false, + "clientRole" : true, + "containerId" : "3fb3e5e5-dbd8-4d51-b964-746c5b2181a4", + "attributes" : { } + }, { "id" : "ba077409-1b5d-4fc8-b20e-10389507fb75", "name" : "basyx-admin", "description" : "", @@ -613,10 +621,27 @@ "clientRole" : true, "containerId" : "049e1323-6efb-4543-bc52-566cd292732a", "attributes" : { } + } ], + "basyx-demo" : [ ], + "workstation-1" : [ { + "id" : "914a18c6-4f14-418f-99e0-bfdcf604ac01", + "name" : "uma_protection", + "composite" : false, + "clientRole" : true, + "containerId" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "attributes" : { } } ] } }, - "groups" : [ ], + "groups" : [ { + "id" : "606a14f2-6114-4fd3-9ca6-4a53514fffb9", + "name" : "BaSyxGroup", + "path" : "/BaSyxGroup", + "attributes" : { }, + "realmRoles" : [ "basyx-deleter", "basyx-creator", "basyx-asset-updater" ], + "clientRoles" : { }, + "subGroups" : [ ] + } ], "defaultRole" : { "id" : "797d2956-a895-4171-ab44-2fc9dbcf7f4c", "name" : "default-roles-basyx", @@ -1364,6 +1389,46 @@ "realmRoles" : [ "visitor", "default-roles-basyx" ], "notBefore" : 0, "groups" : [ ] + }, { + "id" : "a19abcac-34d5-46bb-a604-b07dc234e80f", + "createdTimestamp" : 1715582034760, + "username" : "service-account-workstation-1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "serviceAccountClientId" : "workstation-1", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "basyx-reader", "basyx-deleter", "basyx-updater", "basyx-creator", "default-roles-basyx" ], + "clientRoles" : { + "workstation-1" : [ "uma_protection" ] + }, + "notBefore" : 0, + "groups" : [ "/BaSyxGroup" ] + }, { + "id" : "77957093-d593-44b4-b4e9-bc365e840cdd", + "createdTimestamp" : 1715539640949, + "username" : "test.user", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Test", + "lastName" : "User", + "email" : "test.user@gmail.com", + "credentials" : [ { + "id" : "11672ccf-38b7-421b-8d01-755e0f2197da", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1715539657887, + "secretData" : "{\"value\":\"7YbCeFyuYDpwu/06UbMT7OObo29RU7cG1XWrqcNwZLg=\",\"salt\":\"nAQ9KKDYyg5GGuSUyfG+kw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-basyx" ], + "notBefore" : 0, + "groups" : [ "/BaSyxGroup" ] } ], "scopeMappings" : [ { "clientScope" : "offline_access", @@ -1510,6 +1575,43 @@ "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { + "id" : "615f29de-5f3d-4384-8b71-7351ff2c2a32", + "clientId" : "basyx-demo", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "41f9RznNmtHaWKT7i8IDYTjl2VaLHw0q", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1716897911", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { "id" : "45f21c3d-4e85-466f-984f-d7bd47392453", "clientId" : "broker", "name" : "${client_broker}", @@ -1611,6 +1713,84 @@ } ], "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "96031210-9e6c-4252-a22e-e81a47e30d65", + "clientId" : "workstation-1", + "name" : "Workstation 1", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "nY0mjyECF60DGzNmQUjL81XurSl8etom", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : true, + "authorizationServicesEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1715582034", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "1f332577-69b1-48cf-b2d1-95ccd6159fdf", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "05688508-b82b-46c5-85de-f0bacf03d6e7", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + }, { + "id" : "a1c32e51-4369-4460-a015-98867bb101fd", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] } ], "clientScopes" : [ { "id" : "e0f355da-f9ff-4104-b305-043b0188747b", diff --git a/pom.xml b/pom.xml index 255501885..40d84d01d 100644 --- a/pom.xml +++ b/pom.xml @@ -488,6 +488,11 @@ basyx.submodelservice-http ${revision} + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-client + ${revision} + org.eclipse.digitaltwin.basyx @@ -566,6 +571,11 @@ basyx.aasservice-feature-mqtt ${revision} + + org.eclipse.digitaltwin.basyx + basyx.aasservice-client + ${revision} + org.eclipse.digitaltwin.basyx @@ -831,6 +841,12 @@ ${revision} tests + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-client + ${revision} + tests + org.eclipse.digitaltwin.basyx @@ -918,6 +934,12 @@ ${revision} tests + + org.eclipse.digitaltwin.basyx + basyx.aasservice-client + ${revision} + tests + org.eclipse.digitaltwin.basyx