diff --git a/basyx.aasregistry/README.md b/basyx.aasregistry/README.md index f90df3b50..6fece3dcd 100644 --- a/basyx.aasregistry/README.md +++ b/basyx.aasregistry/README.md @@ -45,10 +45,10 @@ Install maven generate jars: mvn clean install ``` -In order to build the docker images, you need to specify *docker.username* and *docker.password* properties (here without running tests): +In order to build the docker images, you need to specify *docker.namespace* and *docker.password* properties (here without running tests): ``` shell -MAVEN_OPS='-Xmx2048 -Xms1024' mvn clean install -DskipTests -Ddocker.username=eclipsebasyx -Ddocker.password="" +MAVEN_OPS='-Xmx2048 -Xms1024' mvn clean install -DskipTests -Ddocker.namespace=eclipsebasyx -Ddocker.password="" ``` You can now check your images from command-line and push the images: @@ -58,7 +58,7 @@ docker images ... Or you can directly push them from maven. ``` shell -MAVEN_OPS='-Xmx2048 -Xms1024' mvn deploy -Ddocker.registry=docker.io -Ddocker.username=eclipsebasyx -Ddocker.password=pwd +MAVEN_OPS='-Xmx2048 -Xms1024' mvn deploy -Ddocker.registry=docker.io -Ddocker.namespace=eclipsebasyx -Ddocker.password=pwd ``` In addition, maven deploy will also deploy your maven artifacts, so you can do everything in one step. diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md b/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md new file mode 100644 index 000000000..bb12b33c1 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md @@ -0,0 +1,80 @@ +# AssetAdministrationShell Registry - Authorization +This feature enables authorized access to the AssetAdministrationShell Registry. + +To enable this feature, the following properties should be configured: + +``` +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = +basyx.feature.authorization.jwtBearerTokenProvider = +basyx.feature.authorization.rbac.file = +spring.security.oauth2.resourceserver.jwt.issuer-uri= +``` + +Note: Only Role Based Access Control (RBAC) is supported as authorization type as of now, also Keycloak is the only Jwt token provider supported now, and it is also a default provider. + +To know more about the RBAC, please refer [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html) +To know more about the Keycloak server administration, please refer [Server Administration Guide](https://www.keycloak.org/docs/latest/server_admin/#keycloak-features-and-concepts) + +An example valid configuration: + +``` +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 +``` + +## RBAC rule configuration + +For configuring RBAC rules, all the rbac rules should be configured inside a json file, the rules are defined as below: + +``` +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "aas-registry", + "aasId": "*" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "aas-registry", + "aasId": "specificAasId" + } + } + ] +``` + +The role defines which role is allowed to perform the defined actions. The role is as per the configuration of identity providers or based on the organization. Action could be CREATE, READ, UPDATE, DELETE, and EXECUTE, there could be a single action or multiple actions as a list (cf. admin configuration above). + +The targetInformation defines coarse-grained control over the resource, you may define the aasId with a wildcard (\*), it means the defined role x with action y can access any Asset Administration Shell Descriptors on the registry. You can also define a specific AAS Identifier in place of the wildcard (\*), then the role x with action y could be performed only on that particular AAS Descriptor. + +Note: The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE, and EXECUTE) but later user configurable mapping of these actions would be provided. + +## Action table for RBAC + +Below is a reference table that shows which actions are used in what endpoints of the AAS Registry: + +| Action | Endpoint | +|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| READ | GET /shell-descriptors
GET /shell-descriptors/{aasIdentifier}
GET /shell-descriptors/{aasIdentifier}/submodel-descriptors
GET /shell-descriptors/{aasIdentifier}/submodel-descriptors/{submodelIdentifier}
GET /search | +| CREATE | POST /shell-descriptors
| +| UPDATE | PUT /shell-descriptors/{aasIdentifier}
PUT /shell-descriptors/{aasIdentifier}/submodel-descriptors
PUT /shell-descriptors/{aasIdentifier}/submodel-descriptors/{submodelIdentifier}
DELETE /shell-descriptors/{aasIdentifier}/submodel-descriptors/{submodelIdentifier} | +| DELETE | DELETE /shell-descriptors/{aasIdentifier}
DELETE /shell-descriptors | +| EXECUTE | - | + + diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml b/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml new file mode 100644 index 000000000..8f7fc5f5d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry + ${revision} + + + basyx.aasregistry-feature-authorization + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-basemodel + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.http + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + commons-io + commons-io + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-inmemory-storage + test + + + diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AasRegistryTargetInformation.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AasRegistryTargetInformation.java new file mode 100644 index 000000000..c5b065662 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AasRegistryTargetInformation.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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.feature.authorization; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformationSubtype; + +/** + * Specialization of {@link TargetInformation} for Aas Registry target information + * + * @author geso02, danish + * + */ +@TargetInformationSubtype(getValue = "aas-registry") +public class AasRegistryTargetInformation implements TargetInformation { + + private String aasId; + + @JsonCreator + public AasRegistryTargetInformation(final @JsonProperty("aasId") String aasId) { + this.aasId = aasId; + } + + @Override + public Map toMap() { + final Map map = new HashMap<>(); + map.put("aasId", aasId); + return map; + } + + @Override + public int hashCode() { + return Objects.hash(aasId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AasRegistryTargetInformation other = (AasRegistryTargetInformation) obj; + return Objects.equals(aasId, other.aasId); + } + + @Override + public String toString() { + return "AasTargetInformation [aasId=" + aasId + "]"; + } + + public String getAasId() { + return aasId; + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryConfiguration.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryConfiguration.java new file mode 100644 index 000000000..8c34ce5f7 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryConfiguration.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (C) 2023 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.feature.authorization; + +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.rbac.AasRegistryTargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RoleProvider; +import org.eclipse.digitaltwin.basyx.authorization.rbac.SimpleRbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetPermissionVerifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for authorized {@link AuthorizedAasRegistryStorage} + * + * @author geso02, danish + */ +@Configuration +@ConditionalOnExpression("#{${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":false}}") +public class AuthorizedAasRegistryConfiguration { + + @Bean + public TargetPermissionVerifier getAasTargetPermissionVerifier() { + return new AasRegistryTargetPermissionVerifier(); + } + + @Bean + public RbacPermissionResolver getAasPermissionResolver(RbacStorage rbacStorage, RoleProvider roleProvider, TargetPermissionVerifier targetPermissionVerifier) { + return new SimpleRbacPermissionResolver<>(rbacStorage, roleProvider, targetPermissionVerifier); + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryFeature.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryFeature.java new file mode 100644 index 000000000..78a8c8645 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryFeature.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.feature.authorization; + +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorageFeature; +import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * Feature for authorized {@link AasRegistryStorage} + * + * @author geso02 + */ +@Component +@ConditionalOnExpression("#{${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":false}}") +@Order(0) +public class AuthorizedAasRegistryFeature implements AasRegistryStorageFeature { + + @Value("${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":}") + private boolean enabled; + + private RbacPermissionResolver permissionResolver; + + @Autowired + public AuthorizedAasRegistryFeature(RbacPermissionResolver permissionResolver) { + this.permissionResolver = permissionResolver; + } + + @Override + public AasRegistryStorage decorate(AasRegistryStorage storage) { + return new AuthorizedAasRegistryStorage(storage, permissionResolver); + } + + @Override + public String getName() { + return "AasRegistry Authorization"; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryStorage.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryStorage.java new file mode 100644 index 000000000..7a39a67c0 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/AuthorizedAasRegistryStorage.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * 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.feature.authorization; + +import java.util.List; +import java.util.Set; + +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.rbac.AasRegistryTargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetAdministrationShellDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.model.ShellDescriptorSearchRequest; +import org.eclipse.digitaltwin.basyx.aasregistry.model.ShellDescriptorSearchResponse; +import org.eclipse.digitaltwin.basyx.aasregistry.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.AasDescriptorAlreadyExistsException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.AasDescriptorNotFoundException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.SubmodelAlreadyExistsException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.SubmodelNotFoundException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.DescriptorFilter; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; + +/** + * Decorator for authorized {@link AasRegistryStorage} + * + * @author geso02, danish + */ +public class AuthorizedAasRegistryStorage implements AasRegistryStorage { + + private AasRegistryStorage decorated; + private RbacPermissionResolver permissionResolver; + + public AuthorizedAasRegistryStorage(AasRegistryStorage decorated, RbacPermissionResolver permissionResolver) { + this.decorated = decorated; + this.permissionResolver = permissionResolver; + } + + @Override + public CursorResult> getAllAasDescriptors(PaginationInfo pRequest, DescriptorFilter filter) { + assertHasPermission(Action.READ, AasRegistryTargetPermissionVerifier.ALL_ALLOWED_WILDCARD); + return decorated.getAllAasDescriptors(pRequest, filter); + } + + @Override + public AssetAdministrationShellDescriptor getAasDescriptor(String aasDescriptorId) throws AasDescriptorNotFoundException { + assertHasPermission(Action.READ, aasDescriptorId); + return decorated.getAasDescriptor(aasDescriptorId); + } + + @Override + public void insertAasDescriptor(AssetAdministrationShellDescriptor descr) throws AasDescriptorAlreadyExistsException { + assertHasPermission(Action.CREATE, descr.getId()); + decorated.insertAasDescriptor(descr); + } + + @Override + public void replaceAasDescriptor(String aasDescriptorId, AssetAdministrationShellDescriptor descriptor) throws AasDescriptorNotFoundException { + String newId = descriptor.getId(); + + if (!aasDescriptorId.equals(newId)) { + assertHasPermission(Action.DELETE, aasDescriptorId); + assertHasPermission(Action.CREATE, newId); + } else + assertHasPermission(Action.UPDATE, aasDescriptorId); + + decorated.replaceAasDescriptor(aasDescriptorId, descriptor); + } + + @Override + public void removeAasDescriptor(String aasDescriptorId) throws AasDescriptorNotFoundException { + assertHasPermission(Action.DELETE, aasDescriptorId); + decorated.removeAasDescriptor(aasDescriptorId); + } + + @Override + public CursorResult> getAllSubmodels(String aasDescriptorId, PaginationInfo pRequest) throws AasDescriptorNotFoundException { + assertHasPermission(Action.READ, aasDescriptorId); + return decorated.getAllSubmodels(aasDescriptorId, pRequest); + } + + @Override + public SubmodelDescriptor getSubmodel(String aasDescriptorId, String submodelId) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + assertHasPermission(Action.READ, aasDescriptorId); + return decorated.getSubmodel(aasDescriptorId, submodelId); + } + + @Override + public void insertSubmodel(String aasDescriptorId, SubmodelDescriptor submodel) throws AasDescriptorNotFoundException, SubmodelAlreadyExistsException { + assertHasPermission(Action.UPDATE, aasDescriptorId); + decorated.insertSubmodel(aasDescriptorId, submodel); + } + + @Override + public void replaceSubmodel(String aasDescriptorId, String submodelId, SubmodelDescriptor submodel) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + assertHasPermission(Action.UPDATE, aasDescriptorId); + decorated.replaceSubmodel(aasDescriptorId, submodelId, submodel); + } + + @Override + public void removeSubmodel(String aasDescriptorId, String submodelId) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + assertHasPermission(Action.UPDATE, aasDescriptorId); + decorated.removeSubmodel(aasDescriptorId, submodelId); + } + + @Override + public Set clear() { + assertHasPermission(Action.DELETE, AasRegistryTargetPermissionVerifier.ALL_ALLOWED_WILDCARD); + return decorated.clear(); + } + + @Override + public ShellDescriptorSearchResponse searchAasDescriptors(ShellDescriptorSearchRequest request) { + assertHasPermission(Action.READ, AasRegistryTargetPermissionVerifier.ALL_ALLOWED_WILDCARD); + return decorated.searchAasDescriptors(request); + } + + private void assertHasPermission(Action action, String aasId) { + boolean isAuthorized = permissionResolver.hasPermission(action, new AasRegistryTargetInformation(aasId)); + throwExceptionIfInsufficientPermission(isAuthorized); + } + + private void throwExceptionIfInsufficientPermission(boolean isAuthorized) { + if (!isAuthorized) + throw new InsufficientPermissionException("Insufficient Permission: The current subject does not have the required permissions for this operation."); + } +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/AasRegistryTargetPermissionVerifier.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/AasRegistryTargetPermissionVerifier.java new file mode 100644 index 000000000..8997e1fbc --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/AasRegistryTargetPermissionVerifier.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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.feature.authorization.rbac; + +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.AasRegistryTargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetPermissionVerifier; + +/** + * Verifies the {@link AasRegistryTargetInformation} against the {@link RbacRule} + * + * @author geso02 + */ +public class AasRegistryTargetPermissionVerifier implements TargetPermissionVerifier { + + public static final String ALL_ALLOWED_WILDCARD = "*"; + + @Override + public boolean isVerified(RbacRule rbacRule, AasRegistryTargetInformation targetInformation) { + String shellId = targetInformation.getAasId(); + + AasRegistryTargetInformation rbacRuleAasTargetInformation = (AasRegistryTargetInformation) rbacRule.getTargetInformation(); + + return rbacRuleAasTargetInformation.getAasId().equals(ALL_ALLOWED_WILDCARD) || rbacRuleAasTargetInformation.getAasId().equals(shellId); + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/DummyAasRegistryComponent.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/DummyAasRegistryComponent.java new file mode 100644 index 000000000..209da6c13 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/DummyAasRegistryComponent.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * 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.regression.feature.authorization; + +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.AuthorizedAasRegistryStorage; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Creates and starts the {@link AuthorizedAasRegistryStorage} for tests + * + * @author danish + * + */ +@SpringBootApplication(scanBasePackages = { "org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization","org.eclipse.digitaltwin.basyx.aasregistry.service.api","org.eclipse.digitaltwin.basyx.aasregistry.service.events","org.eclipse.digitaltwin.basyx.authorization.rbac", "org.eclipse.digitaltwin.basyx.authorization", "org.eclipse.digitaltwin.basyx.aasregistry.service.configuration", "org.eclipse.digitaltwin.basyx.aasregistry.service.errors" }) +public class DummyAasRegistryComponent { + public static void main(String[] args) { + SpringApplication.run(DummyAasRegistryComponent.class, args); + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java new file mode 100644 index 000000000..df8145fcc --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/TestAuthorizedAasRegistry.java @@ -0,0 +1,764 @@ +/******************************************************************************* + * 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.regression.feature.authorization; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ParseException; +import org.eclipse.digitaltwin.basyx.aasregistry.model.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.DescriptorFilter; +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.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.AuthorizedAasRegistryStorage; +import org.junit.*; +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; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link AuthorizedAasRegistryStorage} feature + * + * @author danish + */ +public class TestAuthorizedAasRegistry { + + private static final String AAS_REGISTRY_PATH = "shell-descriptors"; + private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); + private static final String AAS_DESCRIPTOR_SIMPLE_2_JSON = "authorization/AasDescriptorSimple_2.json"; + private static final String AAS_DESCRIPTOR_SIMPLE_1_JSON = "authorization/AasDescriptorSimple_1.json"; + private static final String SPECIFIC_SHELL_ID_2 = "specificAasId-2"; + private static final String SPECIFIC_SHELL_ID = "dummyShellId_3"; + private static final String SPECIFIC_SUBMODEL_ID = "dummyShellId_3-SM"; + private static AccessTokenProvider tokenProvider; + private static ConfigurableApplicationContext appContext; + private static final String BASE_URL = "http://127.0.0.1:8080"; + public static String aasRegistryBaseUrl = BASE_URL + "/shell-descriptors"; + private static AasRegistryStorage storage; + + @BeforeClass + public static void setUp() throws FileNotFoundException, IOException { + String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + String clientId = "basyx-client-api"; + + tokenProvider = new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + + appContext = new SpringApplication(DummyAasRegistryComponent.class).run(new String[] {}); + + storage = appContext.getBean(AasRegistryStorage.class); + } + + @AfterClass + public static void tearDown() { + appContext.close(); + } + + @Before + public void initializeRepositories() throws FileNotFoundException, IOException { + configureSecurityContext(); + + createDummyShellDescriptorsOnRegistry(5); + + clearSecurityContext(); + } + + @After + public void reset() throws FileNotFoundException, IOException { + configureSecurityContext(); + + Collection descriptors = storage.getAllAasDescriptors(NO_LIMIT_PAGINATION_INFO, new DescriptorFilter(AssetKind.TYPE, "TestAsset")).getResult(); + + descriptors.forEach(descriptor -> storage.removeAasDescriptor(descriptor.getId())); + + clearSecurityContext(); + } + + @Test + public void healthEndpointWithoutAuthorization() throws IOException, ParseException { + String expectedHealthEndpointOutput = getStringFromFile("authorization/HealthOutput.json"); + + String healthEndpointUrl = BASE_URL + "/actuator/health"; + + CloseableHttpResponse healthCheckResponse = BaSyxHttpTestUtils.executeGetOnURL(healthEndpointUrl); + assertEquals(HttpStatus.OK.value(), healthCheckResponse.getCode()); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedHealthEndpointOutput, BaSyxHttpTestUtils.getResponseAsString(healthCheckResponse)); + } + + @Test + public void getAllAasDescriptorsWithCorrectRoleAndPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(aasRegistryBaseUrl, accessToken); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getAllElementsNoAuthorization(aasRegistryBaseUrl); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void createAasDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void createAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void createAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRepositoryWithNoAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndSpecificAasDescriptorPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndUnauthorizedSpecificAasDescriptor() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID_2), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void searchDescriptorsWithCorrectRoleAndPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + System.out.println(BaSyxHttpTestUtils.getResponseAsString(retrievalResponse)); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsCorrectRoleAndSpecificAasPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + System.out.println(BaSyxHttpTestUtils.getResponseAsString(retrievalResponse)); + + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRepositoryWithNoAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json")); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + private String getSearchUrl() { + return BASE_URL + "/search"; + } + + private String getSubmodelDescriptorsAccessURL(String shellId) { + return getSpecificAasDescriptorAccessURL(shellId) + "/submodel-descriptors"; + } + + private String getSpecificSubmodelDescriptorAccessURL(String shellId, String submodelId) { + return getSubmodelDescriptorsAccessURL(shellId) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); + } + + private CloseableHttpResponse deleteElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeDeleteOnURL(url); + } + + private void assertElementIsNotOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.NOT_FOUND.value(), getResponse.getCode()); + } + + private void assertElementExistsOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.OK.value(), getResponse.getCode()); + } + + private CloseableHttpResponse updateElementWithAuthorizationPutRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPutOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithAuthorizationPostRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPutRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePutOnURL(url, aasJsonContent); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPostRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(url, aasJsonContent); + } + + private static CloseableHttpResponse createAasDescriptorOnRepositoryWithNoAuthorization(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(createAasRegistryUrl(url), aasJsonContent); + } + + private CloseableHttpResponse deleteElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedDeleteOnURL(url, accessToken); + } + + private static String getJSONStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private static CloseableHttpResponse createAasDescriptorOnRegistryWithAuthorization(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private String getAccessToken(DummyCredential dummyCredential) { + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private void configureSecurityContext() throws FileNotFoundException, IOException { + String adminToken = getAdminAccessToken(); + + String modulus = getStringFromFile("authorization/modulus.txt"); + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + protected CloseableHttpResponse getElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + protected CloseableHttpResponse getElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getAdminAccessToken() { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private static void createDummyShellDescriptorsOnRegistry(int descriptorsCount) { + List dummyDescriptors = new ArrayList<>(); + for(int i = 0; i < descriptorsCount; i++) + dummyDescriptors.add(createDummyDescriptor("dummyShellId_" + i, "dummyShellIdShort_" + i)); + + dummyDescriptors.forEach(descriptor -> storage.insertAasDescriptor(descriptor)); + } + + private static AssetAdministrationShellDescriptor createDummyDescriptor(String shellId, String shellIdShort) { + + AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(shellId); + descriptor.setIdShort(shellIdShort); + descriptor.setAssetKind(AssetKind.TYPE); + descriptor.setAssetType("TestAsset"); + + setEndpointItem(shellId, descriptor); + descriptor.setGlobalAssetId("DummyGlobalAssetId"); + descriptor.addSubmodelDescriptorsItem(createDummySubmodelDescriptor(shellId + "-SM", shellIdShort + "-SM")); + + return descriptor; + } + + private static SubmodelDescriptor createDummySubmodelDescriptor(String submodelId, String submodelIdShort) { + + SubmodelDescriptor descriptor = new SubmodelDescriptor(submodelId, new ArrayList<>()); + descriptor.setIdShort(submodelIdShort); + + return descriptor; + } + + private static void setEndpointItem(String shellId, AssetAdministrationShellDescriptor descriptor) { + + ProtocolInformation protocolInformation = createProtocolInformation(shellId); + + Endpoint endpoint = new Endpoint("AAS-3.0", protocolInformation); + descriptor.addEndpointsItem(endpoint); + } + + private static ProtocolInformation createProtocolInformation(String shellId) { + String href = String.format("%s/%s", BASE_URL + "/shells", Base64UrlEncodedIdentifier.encodeIdentifier(shellId)); + + ProtocolInformation protocolInformation = new ProtocolInformation(href); + protocolInformation.endpointProtocol(getProtocol(href)); + + return protocolInformation; + } + + private static String getProtocol(String endpoint) { + try { + return new URL(endpoint).getProtocol(); + } catch (MalformedURLException e) { + throw new RuntimeException(); + } + } + + private CloseableHttpResponse getAllElementsWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + private CloseableHttpResponse getAllElementsNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private String getSpecificAasDescriptorAccessURL(String shellId) { + return createAasRegistryUrl(aasRegistryBaseUrl) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(shellId); + } + + private static String createAasRegistryUrl(String aasRepositoryBaseURL) { + + try { + return new URL(new URL(aasRepositoryBaseURL), AAS_REGISTRY_PATH).toString(); + } catch (MalformedURLException e) { + throw new RuntimeException("The AAS Registry Base url is malformed. " + e.getMessage()); + } + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/application.yml b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/application.yml new file mode 100644 index 000000000..279aa7046 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/application.yml @@ -0,0 +1,55 @@ +--- +events: + sink: log +description: + profiles: https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-001 +springdoc: + api-docs: + path: /api-docs +springfox: + documentation: + enabled: true + # open-api.v3.path: /api-docs +management: + endpoints: + web: + exposure: + include: "health,metrics" +logging: + level: + root: INFO +server: + shutdown: graceful + port: 8080 + error: + whitelabel: + enabled: false +spring: + application: + name: Basyx Aas Registry + jackson: + date-format: org.eclipse.digitaltwin.basyx.aasregistry.service.RFC3339DateFormat + serialization: + WRITE_DATES_AS_TIMESTAMPS: false + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:9096/realms/BaSyx + profiles: + active: logEvents,inMemoryStorage + +registry: + type: inMemory + +basyx: + cors: + allowed-origins: "*" + allowed-methods: "GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD" + feature: + authorization: + enabled: true + type: rbac + jwtBearerTokenProvider: keycloak + rbac: + file: classpath:rbac_rules.json \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_1.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_1.json new file mode 100644 index 000000000..8072df62e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_1.json @@ -0,0 +1,27 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId", + "idShort": "dummyShellIdShort_3", + "id": "dummyShellId_3", + "specificAssetIds": null, + "submodelDescriptors": null +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json new file mode 100644 index 000000000..423a700a3 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -0,0 +1,52 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId-Test-2", + "idShort": "specificAasIdShort-2", + "id": "specificAasId-2", + "specificAssetIds": null, + "submodelDescriptors": [ + { + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificAasIdShort-2-SM", + "id": "specificAasId-2-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/c3BlY2lmaWNBYXNJZC0y/submodel-descriptors/c3BlY2lmaWNBYXNJZC0yLVNN", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/ShellDescriptorSearchRequest.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/ShellDescriptorSearchRequest.json new file mode 100644 index 000000000..4b8bd2fa9 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/ShellDescriptorSearchRequest.json @@ -0,0 +1,19 @@ +{ + "page": { + "index": 0, + "size": 10000 + }, + "sortBy": { + "direction": "ASC", + "path": [ + "idShort" + ] + }, + "query": { + "path": "submodelDescriptors.idShort", + "value": "^dummyShellIdShort_.*", + "extensionName": null, + "queryType": "regex", + "combinedWith": null + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json new file mode 100644 index 000000000..80e3c4d15 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-SM", + "id": "dummyShellId_3-1-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1TTQ==", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json new file mode 100644 index 000000000..338719502 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-Update-SM", + "id": "dummyShellId_3-1-Update-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1VcGRhdGUtU00=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/modulus.txt b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/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.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/rbac_rules.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/rbac_rules.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/basyx.aasregistry/basyx.aasregistry-service-basetests/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-basetests/pom.xml index 4bb1ecda9..442641301 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-basetests/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/pom.xml @@ -62,6 +62,25 @@ spring-boot-starter-test + + org.eclipse.digitaltwin.basyx + basyx.authorization + tests + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.http + tests + + + org.apache.httpcomponents.client5 + httpclient5 + + junit junit diff --git a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/AuthorizedAasRegistryTestSuite.java b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/AuthorizedAasRegistryTestSuite.java new file mode 100644 index 000000000..8403d5cec --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/AuthorizedAasRegistryTestSuite.java @@ -0,0 +1,760 @@ +/******************************************************************************* + * 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.tests.integration; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ParseException; +import org.eclipse.digitaltwin.basyx.aasregistry.model.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.DescriptorFilter; +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.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; +import org.junit.*; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +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; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test suite for AAS Registry Authorization + * + * @author danish + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@RunWith(SpringRunner.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public abstract class AuthorizedAasRegistryTestSuite { + + private static final String AAS_REGISTRY_PATH = "shell-descriptors"; + private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(10, null); + private static final String AAS_DESCRIPTOR_SIMPLE_2_JSON = "authorization/AasDescriptorSimple_2.json"; + private static final String AAS_DESCRIPTOR_SIMPLE_1_JSON = "authorization/AasDescriptorSimple_1.json"; + private static final String SPECIFIC_SHELL_ID_2 = "specificAasId-2"; + private static final String SPECIFIC_SHELL_ID = "dummyShellId_3"; + private static final String SPECIFIC_SUBMODEL_ID = "dummyShellId_3-SM"; + private static final String BASE_URL = "http://127.0.0.1:8080"; + private static String aasRegistryBaseUrl = BASE_URL + "/shell-descriptors"; + + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private AasRegistryStorage storage; + + @Before + public void initializeRepositories() throws IOException { + configureSecurityContext(); + + createDummyShellDescriptorsOnRegistry(5); + + clearSecurityContext(); + } + + @After + public void reset() throws IOException { + configureSecurityContext(); + + Collection descriptors = storage.getAllAasDescriptors(NO_LIMIT_PAGINATION_INFO, new DescriptorFilter(AssetKind.TYPE, "TestAsset")).getResult(); + + descriptors.forEach(descriptor -> storage.removeAasDescriptor(descriptor.getId())); + + clearSecurityContext(); + } + + @Test + public void healthEndpointWithoutAuthorization() throws IOException, ParseException { + String expectedHealthEndpointOutput = getStringFromFile("authorization/HealthOutput.json"); + + String healthEndpointUrl = BASE_URL + "/actuator/health"; + + CloseableHttpResponse healthCheckResponse = BaSyxHttpTestUtils.executeGetOnURL(healthEndpointUrl); + assertEquals(HttpStatus.OK.value(), healthCheckResponse.getCode()); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedHealthEndpointOutput, BaSyxHttpTestUtils.getResponseAsString(healthCheckResponse)); + } + + @Test + public void getAllAasDescriptorsWithCorrectRoleAndPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(aasRegistryBaseUrl, accessToken); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getAllElementsNoAuthorization(aasRegistryBaseUrl); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void createAasDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void createAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void createAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRepositoryWithNoAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_1_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteAasDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndSpecificAasDescriptorPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithCorrectRoleAndUnauthorizedSpecificAasDescriptor() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID_2), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, SPECIFIC_SUBMODEL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID_2), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void addSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPostRequest(getSubmodelDescriptorsAccessURL(SPECIFIC_SHELL_ID), getJSONStringFromFile("authorization/SingleSubmodelDescriptor.json")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void removeSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void removeSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createAasDescriptorOnRegistryWithAuthorization(aasRegistryBaseUrl, getJSONStringFromFile(AAS_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void replaceSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_ASSET_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void replaceSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SHELL_ID_2, "specificAasId-2-SM"), getJSONStringFromFile("authorization/SingleSubmodelDescriptor_Update.json")); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(aasRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificAasDescriptorAccessURL(SPECIFIC_SHELL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void searchDescriptorsWithCorrectRoleAndPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + System.out.println(BaSyxHttpTestUtils.getResponseAsString(retrievalResponse)); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsCorrectRoleAndSpecificAasPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + System.out.println(BaSyxHttpTestUtils.getResponseAsString(retrievalResponse)); + + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRegistryWithAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json"), accessToken); + + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void searchDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createAasDescriptorOnRepositoryWithNoAuthorization(getSearchUrl(), getJSONStringFromFile("authorization/ShellDescriptorSearchRequest.json")); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + private String getSearchUrl() { + return BASE_URL + "/search"; + } + + private String getSubmodelDescriptorsAccessURL(String shellId) { + return getSpecificAasDescriptorAccessURL(shellId) + "/submodel-descriptors"; + } + + private String getSpecificSubmodelDescriptorAccessURL(String shellId, String submodelId) { + return getSubmodelDescriptorsAccessURL(shellId) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); + } + + private CloseableHttpResponse deleteElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeDeleteOnURL(url); + } + + private void assertElementIsNotOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.NOT_FOUND.value(), getResponse.getCode()); + } + + private void assertElementExistsOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.OK.value(), getResponse.getCode()); + } + + private CloseableHttpResponse updateElementWithAuthorizationPutRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPutOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithAuthorizationPostRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPutRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePutOnURL(url, aasJsonContent); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPostRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(url, aasJsonContent); + } + + private static CloseableHttpResponse createAasDescriptorOnRepositoryWithNoAuthorization(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(createAasRegistryUrl(url), aasJsonContent); + } + + private CloseableHttpResponse deleteElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedDeleteOnURL(url, accessToken); + } + + private static String getJSONStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private static CloseableHttpResponse createAasDescriptorOnRegistryWithAuthorization(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private String getAccessToken(DummyCredential dummyCredential) { + return getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private void configureSecurityContext() throws IOException { + String adminToken = getAdminAccessToken(); + + String modulus = getStringFromFile("authorization/modulus.txt"); + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + protected CloseableHttpResponse getElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + protected CloseableHttpResponse getElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private String getAdminAccessToken() { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void createDummyShellDescriptorsOnRegistry(int descriptorsCount) { + List dummyDescriptors = new ArrayList<>(); + for(int i = 0; i < descriptorsCount; i++) + dummyDescriptors.add(createDummyDescriptor("dummyShellId_" + i, "dummyShellIdShort_" + i)); + + dummyDescriptors.forEach(descriptor -> storage.insertAasDescriptor(descriptor)); + } + + private static AssetAdministrationShellDescriptor createDummyDescriptor(String shellId, String shellIdShort) { + + AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(shellId); + descriptor.setIdShort(shellIdShort); + descriptor.setAssetKind(AssetKind.TYPE); + descriptor.setAssetType("TestAsset"); + + setEndpointItem(shellId, descriptor); + descriptor.setGlobalAssetId("DummyGlobalAssetId"); + descriptor.addSubmodelDescriptorsItem(createDummySubmodelDescriptor(shellId + "-SM", shellIdShort + "-SM")); + + return descriptor; + } + + private static SubmodelDescriptor createDummySubmodelDescriptor(String submodelId, String submodelIdShort) { + + SubmodelDescriptor descriptor = new SubmodelDescriptor(submodelId, new ArrayList<>()); + descriptor.setIdShort(submodelIdShort); + + return descriptor; + } + + private static void setEndpointItem(String shellId, AssetAdministrationShellDescriptor descriptor) { + + ProtocolInformation protocolInformation = createProtocolInformation(shellId); + + Endpoint endpoint = new Endpoint("AAS-3.0", protocolInformation); + descriptor.addEndpointsItem(endpoint); + } + + private static ProtocolInformation createProtocolInformation(String shellId) { + String href = String.format("%s/%s", BASE_URL + "/shells", Base64UrlEncodedIdentifier.encodeIdentifier(shellId)); + + ProtocolInformation protocolInformation = new ProtocolInformation(href); + protocolInformation.endpointProtocol(getProtocol(href)); + + return protocolInformation; + } + + private static String getProtocol(String endpoint) { + try { + return new URL(endpoint).getProtocol(); + } catch (MalformedURLException e) { + throw new RuntimeException(); + } + } + + private CloseableHttpResponse getAllElementsWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + private CloseableHttpResponse getAllElementsNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private String getSpecificAasDescriptorAccessURL(String shellId) { + return createAasRegistryUrl(aasRegistryBaseUrl) + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(shellId); + } + + private static String createAasRegistryUrl(String aasRepositoryBaseURL) { + + try { + return new URL(new URL(aasRepositoryBaseURL), AAS_REGISTRY_PATH).toString(); + } catch (MalformedURLException e) { + throw new RuntimeException("The AAS Registry Base url is malformed. " + e.getMessage()); + } + } + + private AccessTokenProvider getAccessTokenProvider() { + String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + String clientId = "basyx-client-api"; + + return new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/InMemoryAasStorageConfiguration.java b/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/InMemoryAasStorageConfiguration.java index 3ccc4b4a0..5192abe6a 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/InMemoryAasStorageConfiguration.java +++ b/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/InMemoryAasStorageConfiguration.java @@ -24,7 +24,10 @@ ******************************************************************************/ package org.eclipse.digitaltwin.basyx.aasregistry.service.configuration; +import java.util.List; + import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorageFeature; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.CursorEncodingRegistryStorage; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.memory.InMemoryAasRegistryStorage; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.memory.ThreadSafeAasRegistryStorageDecorator; @@ -32,13 +35,27 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import lombok.extern.log4j.Log4j2; + @Configuration +@Log4j2 public class InMemoryAasStorageConfiguration { + + @Bean @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "inMemory") - public AasRegistryStorage storage() { - return new ThreadSafeAasRegistryStorageDecorator(new CursorEncodingRegistryStorage(new InMemoryAasRegistryStorage())); + public AasRegistryStorage storage(List features) { + log.info("Creating in-memory storage"); + AasRegistryStorage storage = new ThreadSafeAasRegistryStorageDecorator(new CursorEncodingRegistryStorage(new InMemoryAasRegistryStorage())); + return applyFeatures(storage, features); } -} + private AasRegistryStorage applyFeatures(AasRegistryStorage storage, List features) { + for (AasRegistryStorageFeature eachFeature : features) { + log.info("Activating feature " + eachFeature.getName()); + storage = eachFeature.decorate(storage); + } + return storage; + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/InMemoryAasRegistyStorageTest.java b/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/InMemoryAasRegistyStorageTest.java index 85b8cbcdf..09dcafe0e 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/InMemoryAasRegistyStorageTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-inmemory-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/InMemoryAasRegistyStorageTest.java @@ -47,7 +47,7 @@ public class InMemoryAasRegistyStorageTest extends AasRegistryStorageTest { public AasRegistryStorage createCloningInMemoryStorage() { // we save the initial storage state in some testcases // so we do not want to alter the object and thus need a deep copy - return new CloningAasRegistryStorageDecorator(new InMemoryAasStorageConfiguration().storage()); + return new CloningAasRegistryStorageDecorator(new InMemoryAasStorageConfiguration().storage(List.of())); } diff --git a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/MongoDbConfiguration.java b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/MongoDbConfiguration.java index 92df248a8..de16a3a38 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/MongoDbConfiguration.java +++ b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/MongoDbConfiguration.java @@ -24,9 +24,12 @@ ******************************************************************************/ package org.eclipse.digitaltwin.basyx.aasregistry.service.configuration; +import java.util.List; + import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.paths.AasRegistryPaths; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorageFeature; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.CursorEncodingRegistryStorage; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.mongodb.MongoDbAasRegistryStorage; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -40,15 +43,30 @@ import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.scheduling.annotation.EnableAsync; +import lombok.extern.log4j.Log4j2; + @Configuration @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "mongodb") @EnableAsync +@Log4j2 public class MongoDbConfiguration { @Bean - public AasRegistryStorage createAasRegistryStorage(MongoTemplate template) { + public AasRegistryStorage createStorage(MongoTemplate template, List features) { + log.info("Creating mongodb storage"); + log.info("Creating mongodb indices"); initializeIndices(template); - return new CursorEncodingRegistryStorage(new MongoDbAasRegistryStorage(template)); + AasRegistryStorage storage = new CursorEncodingRegistryStorage(new MongoDbAasRegistryStorage(template)); + return applyFeatures(storage, features); + + } + + private AasRegistryStorage applyFeatures(AasRegistryStorage storage, List features) { + for (AasRegistryStorageFeature eachFeature : features) { + log.info("Activating feature " + eachFeature.getName()); + storage = eachFeature.decorate(storage); + } + return storage; } private void initializeIndices(MongoTemplate template) { @@ -69,14 +87,14 @@ private void initializeExtensionIndices(IndexOperations ops) { private void initializeShellExtensionIndices(IndexOperations ops) { initializeSingleAscIndex(ops, AasRegistryPaths.extensions().name()); - initializeSingleAscIndex(ops, AasRegistryPaths.extensions().value()); + initializeSingleAscIndex(ops, AasRegistryPaths.extensions().value()); } private void initializeSubmodelExtensionIndices(IndexOperations ops) { initializeSingleAscIndex(ops, AasRegistryPaths.submodelDescriptors().extensions().name()); initializeSingleAscIndex(ops, AasRegistryPaths.submodelDescriptors().extensions().value()); } - + private void initializeSingleAscIndex(IndexOperations ops, String path) { Index smValueIndex = new Index(path, Direction.ASC); ops.ensureIndex(smValueIndex); diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml index c62f33606..10b50cf46 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml @@ -7,7 +7,6 @@ org.eclipse.digitaltwin.basyx basyx.aasregistry ${revision} - basyx.aasregistry-service-release-kafka-mem @@ -20,11 +19,19 @@ aas-registry-kafka-mem + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx basyx.aasregistry-service-basemodel + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + org.eclipse.digitaltwin.basyx basyx.aasregistry-service-kafka-events diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java new file mode 100644 index 000000000..217f38992 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.memory; + +import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.AuthorizedAasRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Kafka events and InMemory storage + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=kafkaEvents,inMemoryStorage", "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 AuthorizedKafkaInMemoryIntegrationTest extends AuthorizedAasRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_1.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_1.json new file mode 100644 index 000000000..8072df62e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_1.json @@ -0,0 +1,27 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId", + "idShort": "dummyShellIdShort_3", + "id": "dummyShellId_3", + "specificAssetIds": null, + "submodelDescriptors": null +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json new file mode 100644 index 000000000..423a700a3 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -0,0 +1,52 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId-Test-2", + "idShort": "specificAasIdShort-2", + "id": "specificAasId-2", + "specificAssetIds": null, + "submodelDescriptors": [ + { + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificAasIdShort-2-SM", + "id": "specificAasId-2-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/c3BlY2lmaWNBYXNJZC0y/submodel-descriptors/c3BlY2lmaWNBYXNJZC0yLVNN", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json new file mode 100644 index 000000000..4b8bd2fa9 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json @@ -0,0 +1,19 @@ +{ + "page": { + "index": 0, + "size": 10000 + }, + "sortBy": { + "direction": "ASC", + "path": [ + "idShort" + ] + }, + "query": { + "path": "submodelDescriptors.idShort", + "value": "^dummyShellIdShort_.*", + "extensionName": null, + "queryType": "regex", + "combinedWith": null + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json new file mode 100644 index 000000000..80e3c4d15 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-SM", + "id": "dummyShellId_3-1-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1TTQ==", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json new file mode 100644 index 000000000..338719502 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-Update-SM", + "id": "dummyShellId_3-1-Update-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1VcGRhdGUtU00=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/modulus.txt b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/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.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/rbac_rules.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/rbac_rules.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/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml index 6cfba33e7..5788f88f6 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml @@ -19,11 +19,19 @@ aas-registry-kafka-mongodb - + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx basyx.aasregistry-service-mongodb-storage - + org.apache.commons commons-lang3 diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java new file mode 100644 index 000000000..c54caaff0 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.AuthorizedAasRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Kafka events and MongoDB storage + * + * @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 AuthorizedKafkaMongoDBIntegrationTest extends AuthorizedAasRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json new file mode 100644 index 000000000..8072df62e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json @@ -0,0 +1,27 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId", + "idShort": "dummyShellIdShort_3", + "id": "dummyShellId_3", + "specificAssetIds": null, + "submodelDescriptors": null +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json new file mode 100644 index 000000000..423a700a3 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -0,0 +1,52 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId-Test-2", + "idShort": "specificAasIdShort-2", + "id": "specificAasId-2", + "specificAssetIds": null, + "submodelDescriptors": [ + { + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificAasIdShort-2-SM", + "id": "specificAasId-2-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/c3BlY2lmaWNBYXNJZC0y/submodel-descriptors/c3BlY2lmaWNBYXNJZC0yLVNN", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json new file mode 100644 index 000000000..4b8bd2fa9 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json @@ -0,0 +1,19 @@ +{ + "page": { + "index": 0, + "size": 10000 + }, + "sortBy": { + "direction": "ASC", + "path": [ + "idShort" + ] + }, + "query": { + "path": "submodelDescriptors.idShort", + "value": "^dummyShellIdShort_.*", + "extensionName": null, + "queryType": "regex", + "combinedWith": null + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json new file mode 100644 index 000000000..80e3c4d15 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-SM", + "id": "dummyShellId_3-1-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1TTQ==", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json new file mode 100644 index 000000000..338719502 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-Update-SM", + "id": "dummyShellId_3-1-Update-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1VcGRhdGUtU00=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/modulus.txt b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/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.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.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/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml index c5b079d90..83a635fad 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml @@ -19,7 +19,16 @@ org.eclipse.digitaltwin.basyx.aasregistry.service.OpenApiGeneratorApplication aas-registry-log-mem + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx @@ -48,10 +57,16 @@ org.apache.commons commons-lang3 + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-basetests + test + org.springframework.boot spring-boot-starter-test test + diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java new file mode 100644 index 000000000..ec4b515aa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.tests; + +import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.AuthorizedAasRegistryTestSuite; + +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Logging events and InMemory storage + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=logEvents,inMemoryStorage", + "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"}) +public class AuthorizedLogInMemoryIntegrationTest extends AuthorizedAasRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/CorsHeaderTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/CorsHeaderTest.java index 7bf01d569..d99add978 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/CorsHeaderTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/CorsHeaderTest.java @@ -11,16 +11,14 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; -@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @RunWith(SpringRunner.class) -@TestPropertySource(properties = { "spring.profiles.active=logEvents,inMemoryStorage", "basyx.cors.allowed-origins=*", "basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD" }) +@TestPropertySource(properties = {"spring.profiles.active=logEvents,inMemoryStorage", "basyx.cors.allowed-origins=*", "basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD" }) public class CorsHeaderTest { @Value("${local.server.port}") @@ -35,4 +33,4 @@ public void testCorsHeader() throws IOException { assertThat(varyHeaders.contains("Access-Control-Request-Method")); } -} +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_1.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_1.json new file mode 100644 index 000000000..8072df62e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_1.json @@ -0,0 +1,27 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId", + "idShort": "dummyShellIdShort_3", + "id": "dummyShellId_3", + "specificAssetIds": null, + "submodelDescriptors": null +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json new file mode 100644 index 000000000..423a700a3 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -0,0 +1,52 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId-Test-2", + "idShort": "specificAasIdShort-2", + "id": "specificAasId-2", + "specificAssetIds": null, + "submodelDescriptors": [ + { + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificAasIdShort-2-SM", + "id": "specificAasId-2-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/c3BlY2lmaWNBYXNJZC0y/submodel-descriptors/c3BlY2lmaWNBYXNJZC0yLVNN", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json new file mode 100644 index 000000000..4b8bd2fa9 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/ShellDescriptorSearchRequest.json @@ -0,0 +1,19 @@ +{ + "page": { + "index": 0, + "size": 10000 + }, + "sortBy": { + "direction": "ASC", + "path": [ + "idShort" + ] + }, + "query": { + "path": "submodelDescriptors.idShort", + "value": "^dummyShellIdShort_.*", + "extensionName": null, + "queryType": "regex", + "combinedWith": null + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json new file mode 100644 index 000000000..80e3c4d15 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-SM", + "id": "dummyShellId_3-1-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1TTQ==", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json new file mode 100644 index 000000000..338719502 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-Update-SM", + "id": "dummyShellId_3-1-Update-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1VcGRhdGUtU00=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/modulus.txt b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/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.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/rbac_rules.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/rbac_rules.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/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml index 5a51ac74c..396b026ea 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml @@ -19,7 +19,16 @@ org.eclipse.digitaltwin.basyx.aasregistry.service.OpenApiGeneratorApplication aas-registry-log-mongodb + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + org.eclipse.digitaltwin.basyx basyx.aasregistry-service-mongodb-storage @@ -44,6 +53,11 @@ org.apache.commons commons-lang3 + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-basetests + test + diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogMongoDBIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogMongoDBIntegrationTest.java new file mode 100644 index 000000000..0db742555 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/AuthorizedLogMongoDBIntegrationTest.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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.tests; + +import org.eclipse.digitaltwin.basyx.aasregistry.service.tests.integration.AuthorizedAasRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Logging events and MongoDB storage + * + * @author danish + */ +@TestPropertySource(properties = { "spring.profiles.active=logEvents,mongoDbStorage", + "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 AuthorizedLogMongoDBIntegrationTest extends AuthorizedAasRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json new file mode 100644 index 000000000..8072df62e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_1.json @@ -0,0 +1,27 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId", + "idShort": "dummyShellIdShort_3", + "id": "dummyShellId_3", + "specificAssetIds": null, + "submodelDescriptors": null +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json new file mode 100644 index 000000000..423a700a3 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -0,0 +1,52 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "assetKind": "Type", + "assetType": "TestAsset", + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shells/ZHVtbXlTaGVsbElkXzM", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ], + "globalAssetId": "DummyGlobalAssetId-Test-2", + "idShort": "specificAasIdShort-2", + "id": "specificAasId-2", + "specificAssetIds": null, + "submodelDescriptors": [ + { + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificAasIdShort-2-SM", + "id": "specificAasId-2-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/c3BlY2lmaWNBYXNJZC0y/submodel-descriptors/c3BlY2lmaWNBYXNJZC0yLVNN", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json new file mode 100644 index 000000000..4b8bd2fa9 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/ShellDescriptorSearchRequest.json @@ -0,0 +1,19 @@ +{ + "page": { + "index": 0, + "size": 10000 + }, + "sortBy": { + "direction": "ASC", + "path": [ + "idShort" + ] + }, + "query": { + "path": "submodelDescriptors.idShort", + "value": "^dummyShellIdShort_.*", + "extensionName": null, + "queryType": "regex", + "combinedWith": null + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json new file mode 100644 index 000000000..80e3c4d15 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-SM", + "id": "dummyShellId_3-1-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1TTQ==", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json new file mode 100644 index 000000000..338719502 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummyShellIdShort_3-1-Update-SM", + "id": "dummyShellId_3-1-Update-SM", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/shell-descriptors/ZHVtbXlTaGVsbElkXzM/submodel-descriptors/ZHVtbXlTaGVsbElkXzMtMS1VcGRhdGUtU00=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/modulus.txt b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/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.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/rbac_rules.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..d0925041e --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/rbac_rules.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/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/RestConfiguration.java b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/RestConfiguration.java index e836208da..7bcdc98c2 100644 --- a/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/RestConfiguration.java +++ b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/configuration/RestConfiguration.java @@ -26,8 +26,10 @@ import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetKind; import org.eclipse.digitaltwin.basyx.aasregistry.service.api.LocationBuilder; +import org.eclipse.digitaltwin.basyx.http.Aas4JHTTPSerializationExtension; import org.eclipse.digitaltwin.basyx.http.BaSyxHTTPConfiguration; import org.eclipse.digitaltwin.basyx.http.CorsPathPatternProvider; +import org.eclipse.digitaltwin.basyx.http.SerializationExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -61,6 +63,11 @@ public CorsPathPatternProvider getAasRegistryServiceCorsUrlProvider() { return new CorsPathPatternProvider("/shell-descriptors/**"); } + @Bean + public SerializationExtension getExtension() { + return new Aas4JHTTPSerializationExtension(); + } + @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); diff --git a/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/errors/BasyxControllerAdvice.java b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/errors/BasyxControllerAdvice.java index af97f8831..f1fdcfca5 100644 --- a/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/errors/BasyxControllerAdvice.java +++ b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/errors/BasyxControllerAdvice.java @@ -28,12 +28,14 @@ import org.eclipse.digitaltwin.basyx.aasregistry.model.Message; import org.eclipse.digitaltwin.basyx.aasregistry.model.Message.MessageTypeEnum; import org.eclipse.digitaltwin.basyx.aasregistry.model.Result; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.server.ResponseStatusException; @ControllerAdvice @@ -60,6 +62,11 @@ public ResponseEntity handleExceptions(Exception ex) { return newResultEntity(ex, HttpStatus.INTERNAL_SERVER_ERROR); } + @ExceptionHandler(InsufficientPermissionException.class) + public ResponseEntity handleInsufficientPermissionException(InsufficientPermissionException exception) { + return newResultEntity(exception, HttpStatus.FORBIDDEN); + } + private ResponseEntity newResultEntity(Exception ex, HttpStatus status) { Result result = new Result(); Message message = newExceptionMessage(ex.getMessage(), status); diff --git a/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/AasRegistryStorageFeature.java b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/AasRegistryStorageFeature.java new file mode 100644 index 000000000..fae84c47b --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/AasRegistryStorageFeature.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (C) 2023 DFKI GmbH (https://www.dfki.de/en/web) + * + * 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; + +public interface AasRegistryStorageFeature { + + AasRegistryStorage decorate(AasRegistryStorage aasRegistryStorage); + + boolean isEnabled(); + + String getName(); + +} diff --git a/basyx.aasregistry/basyx.aasregistry-service/templates/openapi2SpringBoot.mustache b/basyx.aasregistry/basyx.aasregistry-service/templates/openapi2SpringBoot.mustache new file mode 100644 index 000000000..14b449123 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-service/templates/openapi2SpringBoot.mustache @@ -0,0 +1,34 @@ +package {{basePackage}}; + +{{#openApiNullable}} +import com.fasterxml.jackson.databind.Module; +import org.openapitools.jackson.nullable.JsonNullableModule; +{{/openApiNullable}} +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator; + +@SpringBootApplication( + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +@ComponentScan( + basePackages = {"org.eclipse.digitaltwin.basyx.authorization", "org.eclipse.digitaltwin.basyx.authorization.rbac", "org.eclipse.digitaltwin.basyx.aasregistry.feature", "{{basePackage}}", "{{apiPackage}}" , "{{configPackage}}"}, + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +public class OpenApiGeneratorApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenApiGeneratorApplication.class, args); + } + +{{#openApiNullable}} + @Bean(name = "{{basePackage}}.OpenApiGeneratorApplication.jsonNullableModule") + public Module jsonNullableModule() { + return new JsonNullableModule(); + } +{{/openApiNullable}} + +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/MongoDbAasTransactionsIT.java b/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/MongoDbAasTransactionsIT.java new file mode 100644 index 000000000..57fa7490f --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/java/MongoDbAasTransactionsIT.java @@ -0,0 +1,21 @@ +import org.eclipse.digitaltwin.basyx.aasregistry.service.OpenApiGeneratorApplication; +import org.eclipse.digitaltwin.basyx.aasregistry.service.api.AasRegistryBulkApiController; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +public class MongoDbAasTransactionsIT { + + static ConfigurableApplicationContext appContext; + + @BeforeClass + public static void startAasRegistryEnv() throws Exception { + appContext = new SpringApplication(OpenApiGeneratorApplication.class).run(new String[] {}); + } + + @Test + public void beans() { + AasRegistryBulkApiController service = appContext.getBean(AasRegistryBulkApiController.class); + } +} diff --git a/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/application.properties b/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/application.properties new file mode 100644 index 000000000..02eb95f38 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/application.properties @@ -0,0 +1,4 @@ +registry.type=mongodb +spring.data.mongodb.database=aasregistry +spring.data.mongodb.uri=mongodb://localhost:27018 +events.sink=log diff --git a/basyx.aasregistry/pom.xml b/basyx.aasregistry/pom.xml index d6aa5701e..c190e1ffc 100644 --- a/basyx.aasregistry/pom.xml +++ b/basyx.aasregistry/pom.xml @@ -53,6 +53,7 @@ basyx.aasregistry-service-release-log-mongodb basyx.aasregistry-service-release-kafka-mem basyx.aasregistry-service-release-kafka-mongodb + basyx.aasregistry-feature-authorization @@ -152,6 +153,11 @@ basyx.aasregistry-service-inmemory-storage ${project.version} + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + ${project.version} + org.eclipse.digitaltwin.basyx basyx.aasregistry-service-basetests diff --git a/basyx.common/basyx.http/src/main/java/org/eclipse/digitaltwin/basyx/http/BaSyxExceptionHandler.java b/basyx.common/basyx.http/src/main/java/org/eclipse/digitaltwin/basyx/http/BaSyxExceptionHandler.java index 4abe0273a..10253ea78 100644 --- a/basyx.common/basyx.http/src/main/java/org/eclipse/digitaltwin/basyx/http/BaSyxExceptionHandler.java +++ b/basyx.common/basyx.http/src/main/java/org/eclipse/digitaltwin/basyx/http/BaSyxExceptionHandler.java @@ -111,7 +111,7 @@ public ResponseEntity handleElementNotAFileException(ElementNotAFileExcep } @ExceptionHandler(InsufficientPermissionException.class) - public ResponseEntity handleInsufficientPermissionException(InsufficientPermissionException exception) { + public ResponseEntity handleInsufficientPermissionException(InsufficientPermissionException exception, WebRequest request) { return new ResponseEntity<>(HttpStatus.FORBIDDEN); } diff --git a/basyx.submodelregistry/README.md b/basyx.submodelregistry/README.md index 8f7a32f30..76a3e4a82 100644 --- a/basyx.submodelregistry/README.md +++ b/basyx.submodelregistry/README.md @@ -41,10 +41,10 @@ Install maven generate jars: mvn clean install ``` -In order to build the docker images, you need to specify *docker.username* and *docker.password* properties (here without running tests): +In order to build the docker images, you need to specify *docker.namespace* and *docker.password* properties (here without running tests): ``` shell -MAVEN_OPS='-Xmx2048 -Xms1024' mvn clean install -DskipTests -Ddocker.username=eclipsebasyx -Ddocker.password="" +MAVEN_OPS='-Xmx2048 -Xms1024' mvn clean install -DskipTests -Ddocker.namespace=eclipsebasyx -Ddocker.password="" ``` You can now check your images from command-line and push the images: @@ -54,7 +54,7 @@ docker images ... Or you can directly push them from maven. ``` shell -MAVEN_OPS='-Xmx2048 -Xms1024' mvn deploy -Ddocker.registry=docker.io -Ddocker.username=eclipsebasyx -Ddocker.password=pwd +MAVEN_OPS='-Xmx2048 -Xms1024' mvn deploy -Ddocker.registry=docker.io -Ddocker.namespace=eclipsebasyx -Ddocker.password=pwd ``` In addition, maven deploy will also deploy your maven artifacts, so you can do everything in one step. diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/Readme.md b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/Readme.md new file mode 100644 index 000000000..f7ef16ba7 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/Readme.md @@ -0,0 +1,80 @@ +# Submodel Registry - Authorization +This feature enables authorized access to the Submodel Registry. + +To enable this feature, the following properties should be configured: + +``` +basyx.feature.authorization.enabled = true +basyx.feature.authorization.type = +basyx.feature.authorization.jwtBearerTokenProvider = +basyx.feature.authorization.rbac.file = +spring.security.oauth2.resourceserver.jwt.issuer-uri= +``` + +Note: Only Role Based Access Control (RBAC) is supported as authorization type as of now, also Keycloak is the only Jwt token provider supported now, and it is also a default provider. + +To know more about the RBAC, please refer [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html) +To know more about the Keycloak server administration, please refer [Server Administration Guide](https://www.keycloak.org/docs/latest/server_admin/#keycloak-features-and-concepts) + +An example valid configuration: + +``` +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 +``` + +## RBAC rule configuration + +For configuring RBAC rules, all the rbac rules should be configured inside a json file, the rules are defined as below: + +``` +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE"], + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "*" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel-registry", + "submodelId": "specificSubmodelId" + } + } + ] +``` + +The role defines which role is allowed to perform the defined actions. The role is as per the configuration of identity providers or based on the organization. Action could be CREATE, READ, UPDATE, DELETE, and EXECUTE, there could be a single action or multiple actions as a list (cf. admin configuration above). + +The targetInformation defines coarse-grained control over the resource, you may define the submodelId with a wildcard (\*), it means the defined role x with action y can access any Submodel Descriptors on the registry. You can also define a specific Submodel Identifier in place of the wildcard (\*), then the role x with action y could be performed only on that particular Submodel Descriptor. + +Note: The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE, and EXECUTE) but later user configurable mapping of these actions would be provided. + +## Action table for RBAC + +Below is a reference table that shows which actions are used in what endpoints of the Submodel Registry: + +| Action | Endpoint | +|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| READ | GET /submodel-descriptors
GET /submodel-descriptors/{submodelIdentifier} | +| CREATE | POST /submodel-descriptors
| +| UPDATE | PUT /submodel-descriptors/{submodelIdentifier} | +| DELETE | DELETE /submodel-descriptors/{submodelIdentifier}
DELETE /submodel-descriptors | +| EXECUTE | - | + + diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml new file mode 100644 index 000000000..4d05ded4e --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry + ${revision} + + + basyx.submodelregistry-feature-authorization + + + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-service + + + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-service-basemodel + + + org.eclipse.digitaltwin.basyx + basyx.http + test + tests + + + org.eclipse.digitaltwin.basyx + basyx.http + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + commons-io + commons-io + + + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-service-inmemory-storage + test + + + diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryConfiguration.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryConfiguration.java new file mode 100644 index 000000000..4e8532b5e --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryConfiguration.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.submodelregistry.feature.authorization; + +import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RoleProvider; +import org.eclipse.digitaltwin.basyx.authorization.rbac.SimpleRbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization.rbac.SubmodelRegistryTargetPermissionVerifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for authorized {@link AuthorizedSubmodelRegistryStorage} + * + * @author danish + */ +@Configuration +@ConditionalOnExpression("#{${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":false}}") +public class AuthorizedSubmodelRegistryConfiguration { + + @Bean + public TargetPermissionVerifier getSubmodelRegistryTargetPermissionVerifier() { + return new SubmodelRegistryTargetPermissionVerifier(); + } + + @Bean + public RbacPermissionResolver getSubmodelRegistryPermissionResolver(RbacStorage rbacStorage, RoleProvider roleProvider, TargetPermissionVerifier targetPermissionVerifier) { + return new SimpleRbacPermissionResolver<>(rbacStorage, roleProvider, targetPermissionVerifier); + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryFeature.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryFeature.java new file mode 100644 index 000000000..8cda4dc73 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryFeature.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.feature.authorization; + +import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorageFeature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * Feature for authorized {@link SubmodelRegistryStorage} + * + * @author danish + */ +@Component +@ConditionalOnExpression("#{${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":false}}") +@Order(0) +public class AuthorizedSubmodelRegistryFeature implements SubmodelRegistryStorageFeature { + + @Value("${" + CommonAuthorizationProperties.ENABLED_PROPERTY_KEY + ":}") + private boolean enabled; + + private RbacPermissionResolver permissionResolver; + + @Autowired + public AuthorizedSubmodelRegistryFeature(RbacPermissionResolver permissionResolver) { + this.permissionResolver = permissionResolver; + } + + @Override + public SubmodelRegistryStorage decorate(SubmodelRegistryStorage storage) { + return new AuthorizedSubmodelRegistryStorage(storage, permissionResolver); + } + + @Override + public String getName() { + return "SubmodelRegistry Authorization"; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryStorage.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryStorage.java new file mode 100644 index 000000000..694c1f0d1 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/AuthorizedSubmodelRegistryStorage.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.feature.authorization; + +import java.util.List; +import java.util.Set; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization.rbac.SubmodelRegistryTargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.errors.SubmodelAlreadyExistsException; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.errors.SubmodelNotFoundException; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; + +/** + * Decorator for authorized {@link SubmodelRegistryStorage} + * + * @author danish + */ +public class AuthorizedSubmodelRegistryStorage implements SubmodelRegistryStorage { + + private SubmodelRegistryStorage decorated; + private RbacPermissionResolver permissionResolver; + + public AuthorizedSubmodelRegistryStorage(SubmodelRegistryStorage decorated, RbacPermissionResolver permissionResolver) { + this.decorated = decorated; + this.permissionResolver = permissionResolver; + } + + @Override + public CursorResult> getAllSubmodelDescriptors(PaginationInfo pRequest) { + assertHasPermission(Action.READ, SubmodelRegistryTargetPermissionVerifier.ALL_ALLOWED_WILDCARD); + return decorated.getAllSubmodelDescriptors(pRequest); + } + + @Override + public SubmodelDescriptor getSubmodelDescriptor(String submodelId) throws SubmodelNotFoundException { + assertHasPermission(Action.READ, submodelId); + return decorated.getSubmodelDescriptor(submodelId); + } + + @Override + public void removeSubmodelDescriptor(String submodelId) throws SubmodelNotFoundException { + assertHasPermission(Action.DELETE, submodelId); + decorated.removeSubmodelDescriptor(submodelId); + } + + public void replaceSubmodelDescriptor(String submodelId, SubmodelDescriptor descriptor) throws SubmodelNotFoundException { + String newId = descriptor.getId(); + + if (!submodelId.equals(newId)) { + assertHasPermission(Action.DELETE, submodelId); + assertHasPermission(Action.CREATE, newId); + } + else + assertHasPermission(Action.UPDATE, submodelId); + + decorated.replaceSubmodelDescriptor(submodelId, descriptor); + } + + @Override + public void insertSubmodelDescriptor(SubmodelDescriptor descriptor) throws SubmodelAlreadyExistsException { + assertHasPermission(Action.CREATE, descriptor.getId()); + decorated.insertSubmodelDescriptor(descriptor); + } + + @Override + public Set clear() { + assertHasPermission(Action.DELETE, SubmodelRegistryTargetPermissionVerifier.ALL_ALLOWED_WILDCARD); + return decorated.clear(); + } + + private void assertHasPermission(Action action, String submodelId) { + boolean isAuthorized = permissionResolver.hasPermission(action, new SubmodelRegistryTargetInformation(submodelId)); + throwExceptionIfInsufficientPermission(isAuthorized); + } + + private void throwExceptionIfInsufficientPermission(boolean isAuthorized) { + if (!isAuthorized) + throw new InsufficientPermissionException("Insufficient Permission: The current subject does not have the required permissions for this operation."); + } +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/SubmodelRegistryTargetInformation.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/SubmodelRegistryTargetInformation.java new file mode 100644 index 000000000..ac4cd9f7d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/SubmodelRegistryTargetInformation.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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.feature.authorization; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformationSubtype; + +/** + * Specialization of {@link TargetInformation} for Submodel Registry target information + * + * @author danish + * + */ +@TargetInformationSubtype(getValue = "submodel-registry") +public class SubmodelRegistryTargetInformation implements TargetInformation { + + private String submodelId; + + @JsonCreator + public SubmodelRegistryTargetInformation(final @JsonProperty("submodelId") String submodelId) { + this.submodelId = submodelId; + } + + @Override + public Map toMap() { + final Map map = new HashMap<>(); + map.put("submodelId", submodelId); + return map; + } + + @Override + public int hashCode() { + return Objects.hash(submodelId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SubmodelRegistryTargetInformation other = (SubmodelRegistryTargetInformation) obj; + return Objects.equals(submodelId, other.submodelId); + } + + @Override + public String toString() { + return "SubmodelTargetInformation [submodelId=" + submodelId + "]"; + } + + public String getSubmodelId() { + return submodelId; + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/rbac/SubmodelRegistryTargetPermissionVerifier.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/rbac/SubmodelRegistryTargetPermissionVerifier.java new file mode 100644 index 000000000..a68400692 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/feature/authorization/rbac/SubmodelRegistryTargetPermissionVerifier.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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.feature.authorization.rbac; + +import org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization.SubmodelRegistryTargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetPermissionVerifier; + +/** + * Verifies the {@link SubmodelRegistryTargetInformation} against the {@link RbacRule} + * + * @author danish + */ +public class SubmodelRegistryTargetPermissionVerifier implements TargetPermissionVerifier { + + public static final String ALL_ALLOWED_WILDCARD = "*"; + + @Override + public boolean isVerified(RbacRule rbacRule, SubmodelRegistryTargetInformation targetInformation) { + String submodelId = targetInformation.getSubmodelId(); + + SubmodelRegistryTargetInformation rbacRuleSubmodelTargetInformation = (SubmodelRegistryTargetInformation) rbacRule.getTargetInformation(); + + return rbacRuleSubmodelTargetInformation.getSubmodelId().equals(ALL_ALLOWED_WILDCARD) || rbacRuleSubmodelTargetInformation.getSubmodelId().equals(submodelId); + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/DummySubmodelRegistryComponent.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/DummySubmodelRegistryComponent.java new file mode 100644 index 000000000..ff4acc493 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/DummySubmodelRegistryComponent.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * 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.regression.feature.authorization; + +import org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization.AuthorizedSubmodelRegistryStorage; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Creates and starts the {@link AuthorizedSubmodelRegistryStorage} for tests + * + * @author danish + * + */ +@SpringBootApplication(scanBasePackages = {"org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization","org.eclipse.digitaltwin.basyx.submodelregistry.service.api","org.eclipse.digitaltwin.basyx.submodelregistry.service.events","org.eclipse.digitaltwin.basyx.authorization.rbac", "org.eclipse.digitaltwin.basyx.authorization", "org.eclipse.digitaltwin.basyx.submodelregistry.service.configuration","org.eclipse.digitaltwin.basyx.submodelregistry.service.errors"}) +public class DummySubmodelRegistryComponent { + public static void main(String[] args) { + SpringApplication.run(DummySubmodelRegistryComponent.class, args); + } +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java new file mode 100644 index 000000000..37f359a32 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/regression/feature/authorization/TestAuthorizedSubmodelRegistry.java @@ -0,0 +1,501 @@ +/******************************************************************************* + * 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.regression.feature.authorization; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ParseException; +import org.eclipse.digitaltwin.basyx.submodelregistry.feature.authorization.AuthorizedSubmodelRegistryStorage; +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.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; +import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.Endpoint; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.ProtocolInformation; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; +import org.junit.*; +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; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link AuthorizedSubmodelRegistryStorage} feature + * + * @author danish + */ +public class TestAuthorizedSubmodelRegistry { + + private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(0, null); + private static final String SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON = "authorization/SubmodelDescriptorSimple_2.json"; + private static final String SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON = "authorization/SubmodelDescriptorSimple_1.json"; + private static final String SPECIFIC_SUBMODEL_ID_2 = "specificSubmodelId-2"; + private static final String SPECIFIC_SUBMODEL_ID = "dummySubmodelId_3"; + private static AccessTokenProvider tokenProvider; + private static ConfigurableApplicationContext appContext; + private static final String BASE_URL = "http://127.0.0.1:8080"; + public static String submodelRegistryBaseUrl = BASE_URL + "/submodel-descriptors"; + private static SubmodelRegistryStorage storage; + + @BeforeClass + public static void setUp() throws FileNotFoundException, IOException { + String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + String clientId = "basyx-client-api"; + + tokenProvider = new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + + appContext = new SpringApplication(DummySubmodelRegistryComponent.class).run(new String[] {}); + + storage = appContext.getBean(SubmodelRegistryStorage.class); + } + + @AfterClass + public static void tearDown() { + appContext.close(); + } + + @Before + public void initializeRepositories() throws IOException { + configureSecurityContext(); + + createDummySubmodelDescriptorsOnRegistry(5); + + clearSecurityContext(); + } + + @After + public void reset() throws IOException { + configureSecurityContext(); + + Collection descriptors = storage.getAllSubmodelDescriptors(NO_LIMIT_PAGINATION_INFO).getResult(); + + descriptors.forEach(descriptor -> storage.removeSubmodelDescriptor(descriptor.getId())); + + clearSecurityContext(); + } + + @Test + public void healthEndpointWithoutAuthorization() throws IOException, ParseException { + String expectedHealthEndpointOutput = getStringFromFile("authorization/HealthOutput.json"); + + String healthEndpointUrl = BASE_URL + "/actuator/health"; + + CloseableHttpResponse healthCheckResponse = BaSyxHttpTestUtils.executeGetOnURL(healthEndpointUrl); + assertEquals(HttpStatus.OK.value(), healthCheckResponse.getCode()); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedHealthEndpointOutput, BaSyxHttpTestUtils.getResponseAsString(healthCheckResponse)); + } + + @Test + public void getAllSubmodelDescriptorsWithCorrectRoleAndPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(submodelRegistryBaseUrl, accessToken); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllSubmodelDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getAllElementsNoAuthorization(submodelRegistryBaseUrl); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void createSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void createSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void createSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createSubmodelDescriptorOnRepositoryWithNoAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + private CloseableHttpResponse deleteElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeDeleteOnURL(url); + } + + private void assertElementIsNotOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.NOT_FOUND.value(), getResponse.getCode()); + } + + private void assertElementExistsOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.OK.value(), getResponse.getCode()); + } + + private CloseableHttpResponse updateElementWithAuthorizationPutRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPutOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPutRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePutOnURL(url, aasJsonContent); + } + + private static CloseableHttpResponse createSubmodelDescriptorOnRepositoryWithNoAuthorization(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(url, aasJsonContent); + } + + private CloseableHttpResponse deleteElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedDeleteOnURL(url, accessToken); + } + + private static String getJSONStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private static CloseableHttpResponse createElementOnRegistryWithAuthorization(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private String getAccessToken(DummyCredential dummyCredential) { + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private void configureSecurityContext() throws FileNotFoundException, IOException { + String adminToken = getAdminAccessToken(); + + String modulus = getStringFromFile("authorization/modulus.txt"); + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + protected CloseableHttpResponse getElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + protected CloseableHttpResponse getElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getAdminAccessToken() { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return tokenProvider.getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private static void createDummySubmodelDescriptorsOnRegistry(int descriptorsCount) { + List dummyDescriptors = new ArrayList<>(); + for(int i = 0; i < descriptorsCount; i++) + dummyDescriptors.add(createDummyDescriptor("dummySubmodelId_" + i, "dummySubmodelIdShort_" + i)); + + dummyDescriptors.forEach(descriptor -> storage.insertSubmodelDescriptor(descriptor)); + } + + private static SubmodelDescriptor createDummyDescriptor(String submodelId, String submodelIdShort) { + SubmodelDescriptor descriptor = new SubmodelDescriptor(submodelId, new ArrayList<>()); + descriptor.setIdShort(submodelIdShort); + + setEndpointItem(submodelId, descriptor); + + return descriptor; + } + + private static void setEndpointItem(String submodelId, SubmodelDescriptor descriptor) { + + ProtocolInformation protocolInformation = createProtocolInformation(submodelId); + + Endpoint endpoint = new Endpoint("AAS-3.0", protocolInformation); + descriptor.addEndpointsItem(endpoint); + } + + private static ProtocolInformation createProtocolInformation(String submodelId) { + String href = String.format("%s/%s", BASE_URL + "/submodels", Base64UrlEncodedIdentifier.encodeIdentifier(submodelId)); + + ProtocolInformation protocolInformation = new ProtocolInformation(href); + protocolInformation.endpointProtocol(getProtocol(href)); + + return protocolInformation; + } + + private static String getProtocol(String endpoint) { + try { + return new URL(endpoint).getProtocol(); + } catch (MalformedURLException e) { + throw new RuntimeException(); + } + } + + private CloseableHttpResponse getAllElementsWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + private CloseableHttpResponse getAllElementsNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getStringFromFile(String fileName) throws IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private String getSpecificSubmodelDescriptorAccessURL(String submodelId) { + return submodelRegistryBaseUrl + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/application.yml b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/application.yml new file mode 100644 index 000000000..484de9db4 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/application.yml @@ -0,0 +1,55 @@ +--- +events: + sink: log +description: + profiles: https://admin-shell.io/aas/API/3/0/SubmodelRegistryServiceSpecification/SSP-001 +springdoc: + api-docs: + path: /api-docs +springfox: + documentation: + enabled: true + # open-api.v3.path: /api-docs +management: + endpoints: + web: + exposure: + include: "health,metrics" +logging: + level: + root: INFO +server: + shutdown: graceful + port: 8080 + error: + whitelabel: + enabled: false +spring: + application: + name: Basyx Submodel Registry + jackson: + date-format: org.eclipse.digitaltwin.basyx.submodelregistry.service.RFC3339DateFormat + serialization: + WRITE_DATES_AS_TIMESTAMPS: false + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:9096/realms/BaSyx + profiles: + active: logEvents,inMemoryStorage + +registry: + type: inMemory + +basyx: + cors: + allowed-origins: "*" + allowed-methods: "GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD" + feature: + authorization: + enabled: true + type: rbac + jwtBearerTokenProvider: keycloak + rbac: + file: classpath:rbac_rules.json \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json new file mode 100644 index 000000000..e9c5d704a --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummySubmodelIdShort_3", + "id": "dummySubmodelId_3", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/ZHVtbXlTdWJtb2RlbElkXzM=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json new file mode 100644 index 000000000..370b78932 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificSubmodelIdShort-2", + "id": "specificSubmodelId-2", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/c3BlY2lmaWNTdWJtb2RlbElkLTI=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/modulus.txt b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/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.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/rbac_rules.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/rbac_rules.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/basyx.submodelregistry/basyx.submodelregistry-service-basetests/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/pom.xml index d7ed81e1c..d323dd365 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/pom.xml @@ -72,5 +72,24 @@
+ + + org.eclipse.digitaltwin.basyx + basyx.authorization + tests + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.http + tests + + + org.apache.httpcomponents.client5 + httpclient5 +
diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/AuthorizedSubmodelRegistryTestSuite.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/AuthorizedSubmodelRegistryTestSuite.java new file mode 100644 index 000000000..5c20a12b7 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/AuthorizedSubmodelRegistryTestSuite.java @@ -0,0 +1,497 @@ +/******************************************************************************* + * 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.tests.integration; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +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.submodelregistry.model.Endpoint; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.ProtocolInformation; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; +import org.junit.*; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.http.HttpStatus; +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 java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.hc.core5.http.ParseException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; + +/** + * Test suite for Submodel Registry Authorization + * + * @author danish + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@RunWith(SpringRunner.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public abstract class AuthorizedSubmodelRegistryTestSuite { + + private static final PaginationInfo NO_LIMIT_PAGINATION_INFO = new PaginationInfo(10, null); + private static final String SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON = "authorization/SubmodelDescriptorSimple_2.json"; + private static final String SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON = "authorization/SubmodelDescriptorSimple_1.json"; + private static final String SPECIFIC_SUBMODEL_ID_2 = "specificSubmodelId-2"; + private static final String SPECIFIC_SUBMODEL_ID = "dummySubmodelId_3"; + private static final String BASE_URL = "http://127.0.0.1:8080"; + public static String submodelRegistryBaseUrl = BASE_URL + "/submodel-descriptors"; + + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private SubmodelRegistryStorage storage; + + @Before + public void initializeRepositories() throws IOException { + configureSecurityContext(); + + createDummySubmodelDescriptorsOnRegistry(5); + + clearSecurityContext(); + } + + @After + public void reset() throws IOException { + configureSecurityContext(); + + Collection descriptors = storage.getAllSubmodelDescriptors(NO_LIMIT_PAGINATION_INFO).getResult(); + + descriptors.forEach(descriptor -> storage.removeSubmodelDescriptor(descriptor.getId())); + + clearSecurityContext(); + } + + @Test + public void healthEndpointWithoutAuthorization() throws IOException, ParseException { + String expectedHealthEndpointOutput = getStringFromFile("authorization/HealthOutput.json"); + + String healthEndpointUrl = BASE_URL + "/actuator/health"; + + CloseableHttpResponse healthCheckResponse = BaSyxHttpTestUtils.executeGetOnURL(healthEndpointUrl); + assertEquals(HttpStatus.OK.value(), healthCheckResponse.getCode()); + + BaSyxHttpTestUtils.assertSameJSONContent(expectedHealthEndpointOutput, BaSyxHttpTestUtils.getResponseAsString(healthCheckResponse)); + } + + @Test + public void getAllSubmodelDescriptorsWithCorrectRoleAndPermission() throws IOException, ParseException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(submodelRegistryBaseUrl, accessToken); + + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllSubmodelDescriptorsWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getAllElementsWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getAllAasDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getAllElementsNoAuthorization(submodelRegistryBaseUrl); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void createSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.CREATED.value(), retrievalResponse.getCode()); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void createSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void createSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = createSubmodelDescriptorOnRepositoryWithNoAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void updateSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_1_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + createElementOnRegistryWithAuthorization(submodelRegistryBaseUrl, getJSONStringFromFile(SUBMODEL_DESCRIPTOR_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + + deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void deleteSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndSpecificAasPermission() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.OK.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID_2), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithInsufficientPermissionRole() throws IOException { + DummyCredential dummyCredential = DummyCredentialStore.BASYX_UPDATER_CREDENTIAL; + + String accessToken = getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + + CloseableHttpResponse retrievalResponse = getElementWithAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void getSubmodelDescriptorWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + + assertElementIsNotOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndSpecificAasPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithCorrectRoleAndUnauthorizedSpecificAas() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_DELETER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(submodelRegistryBaseUrl, accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + @Test + public void clearAllDescriptorsWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + + assertElementExistsOnServer(getSpecificSubmodelDescriptorAccessURL(SPECIFIC_SUBMODEL_ID), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); + } + + private CloseableHttpResponse deleteElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeDeleteOnURL(url); + } + + private void assertElementIsNotOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.NOT_FOUND.value(), getResponse.getCode()); + } + + private void assertElementExistsOnServer(String url, String accessToken) throws IOException { + CloseableHttpResponse getResponse = getElementWithAuthorization(url, accessToken); + assertEquals(HttpStatus.OK.value(), getResponse.getCode()); + } + + private CloseableHttpResponse updateElementWithAuthorizationPutRequest(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPutOnURL(url, aasJsonContent, accessToken); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPutRequest(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePutOnURL(url, aasJsonContent); + } + + private static CloseableHttpResponse createSubmodelDescriptorOnRepositoryWithNoAuthorization(String url, String aasJsonContent) throws IOException { + return BaSyxHttpTestUtils.executePostOnURL(url, aasJsonContent); + } + + private CloseableHttpResponse deleteElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedDeleteOnURL(url, accessToken); + } + + private static String getJSONStringFromFile(String fileName) throws FileNotFoundException, IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private static CloseableHttpResponse createElementOnRegistryWithAuthorization(String url, String aasJsonContent, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPostOnURL(url, aasJsonContent, accessToken); + } + + private String getAccessToken(DummyCredential dummyCredential) { + return getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + private void configureSecurityContext() throws FileNotFoundException, IOException { + String adminToken = getAdminAccessToken(); + + String modulus = getStringFromFile("authorization/modulus.txt"); + String exponent = "AQAB"; + + RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent); + + Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey); + + SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt)); + } + + protected CloseableHttpResponse getElementWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + protected CloseableHttpResponse getElementWithNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private String getAdminAccessToken() { + DummyCredential dummyCredential = DummyCredentialStore.ADMIN_CREDENTIAL; + + return getAccessTokenProvider().getAccessToken(dummyCredential.getUsername(), dummyCredential.getPassword()); + } + + private void createDummySubmodelDescriptorsOnRegistry(int descriptorsCount) { + List dummyDescriptors = new ArrayList<>(); + for(int i = 0; i < descriptorsCount; i++) + dummyDescriptors.add(createDummyDescriptor("dummySubmodelId_" + i, "dummySubmodelIdShort_" + i)); + + dummyDescriptors.forEach(descriptor -> storage.insertSubmodelDescriptor(descriptor)); + } + + private static SubmodelDescriptor createDummyDescriptor(String submodelId, String submodelIdShort) { + SubmodelDescriptor descriptor = new SubmodelDescriptor(submodelId, new ArrayList<>()); + descriptor.setIdShort(submodelIdShort); + + setEndpointItem(submodelId, descriptor); + + return descriptor; + } + + private static void setEndpointItem(String submodelId, SubmodelDescriptor descriptor) { + + ProtocolInformation protocolInformation = createProtocolInformation(submodelId); + + Endpoint endpoint = new Endpoint("AAS-3.0", protocolInformation); + descriptor.addEndpointsItem(endpoint); + } + + private static ProtocolInformation createProtocolInformation(String submodelId) { + String href = String.format("%s/%s", BASE_URL + "/submodels", Base64UrlEncodedIdentifier.encodeIdentifier(submodelId)); + + ProtocolInformation protocolInformation = new ProtocolInformation(href); + protocolInformation.endpointProtocol(getProtocol(href)); + + return protocolInformation; + } + + private static String getProtocol(String endpoint) { + try { + return new URL(endpoint).getProtocol(); + } catch (MalformedURLException e) { + throw new RuntimeException(); + } + } + + private CloseableHttpResponse getAllElementsWithAuthorization(String url, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedGetOnURL(url, accessToken); + } + + private CloseableHttpResponse getAllElementsNoAuthorization(String url) throws IOException { + return BaSyxHttpTestUtils.executeGetOnURL(url); + } + + private static String getStringFromFile(String fileName) throws IOException { + return BaSyxHttpTestUtils.readJSONStringFromClasspath(fileName); + } + + private String getSpecificSubmodelDescriptorAccessURL(String submodelId) { + return submodelRegistryBaseUrl + "/" + Base64UrlEncodedIdentifier.encodeIdentifier(submodelId); + } + + private AccessTokenProvider getAccessTokenProvider() { + String authenticaltionServerTokenEndpoint = "http://localhost:9096/realms/BaSyx/protocol/openid-connect/token"; + String clientId = "basyx-client-api"; + + return new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId); + } + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/InMemorySubmodelStorageConfiguration.java b/basyx.submodelregistry/basyx.submodelregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/InMemorySubmodelStorageConfiguration.java index e03d4bfbd..653f08631 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/InMemorySubmodelStorageConfiguration.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-inmemory-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/InMemorySubmodelStorageConfiguration.java @@ -24,21 +24,39 @@ ******************************************************************************/ package org.eclipse.digitaltwin.basyx.submodelregistry.service.configuration; +import lombok.extern.log4j.Log4j2; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.CursorEncodingRegistryStorage; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorageFeature; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.memory.InMemorySubmodelRegistryStorage; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.memory.ThreadSafeSubmodelRegistryStorageDecorator; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + @Configuration +@Log4j2 public class InMemorySubmodelStorageConfiguration { @Bean @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "inMemory") - public SubmodelRegistryStorage storage() { - return new ThreadSafeSubmodelRegistryStorageDecorator(new CursorEncodingRegistryStorage(new InMemorySubmodelRegistryStorage())); + public SubmodelRegistryStorage storage(List features) { + log.info("Creating in-memory storage"); + + SubmodelRegistryStorage storage = new ThreadSafeSubmodelRegistryStorageDecorator(new CursorEncodingRegistryStorage(new InMemorySubmodelRegistryStorage())); + + return applyFeatures(storage, features); + } + + private SubmodelRegistryStorage applyFeatures(SubmodelRegistryStorage storage, List features) { + for (SubmodelRegistryStorageFeature eachFeature : features) { + log.info("Activating feature " + eachFeature.getName()); + storage = eachFeature.decorate(storage); + } + + return storage; } } diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/MongoDbConfiguration.java b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/MongoDbConfiguration.java index f2884625d..0f8df5801 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/MongoDbConfiguration.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/MongoDbConfiguration.java @@ -24,8 +24,10 @@ ******************************************************************************/ package org.eclipse.digitaltwin.basyx.submodelregistry.service.configuration; +import lombok.extern.log4j.Log4j2; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.CursorEncodingRegistryStorage; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorageFeature; import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.mongodb.MongoDbSubmodelRegistryStorage; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -35,18 +37,35 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.scheduling.annotation.EnableAsync; +import java.util.List; + @Configuration @ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "mongodb") @EnableAsync +@Log4j2 public class MongoDbConfiguration { @Bean - public SubmodelRegistryStorage createSubmodelRegistryStorage(MongoTemplate template) { - return new CursorEncodingRegistryStorage(new MongoDbSubmodelRegistryStorage(template)); + public SubmodelRegistryStorage createSubmodelRegistryStorage(MongoTemplate template, List features) { + log.info("Creating mongodb storage"); + + SubmodelRegistryStorage storage = new CursorEncodingRegistryStorage(new MongoDbSubmodelRegistryStorage(template)); + + return applyFeatures(storage, features); } @Bean public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { return new MongoTransactionManager(dbFactory); } + + private SubmodelRegistryStorage applyFeatures(SubmodelRegistryStorage storage, List features) { + for (SubmodelRegistryStorageFeature eachFeature : features) { + log.info("Activating feature " + eachFeature.getName()); + storage = eachFeature.decorate(storage); + } + + return storage; + } + } \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/pom.xml index c05399a7f..66f1c919c 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/pom.xml @@ -20,6 +20,14 @@ submodel-registry-kafka-mem + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java new file mode 100644 index 000000000..77d05607c --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/memory/AuthorizedKafkaInMemoryIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.memory; + +import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.AuthorizedSubmodelRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Kafka events and InMemory storage + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=kafkaEvents,inMemoryStorage", "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 AuthorizedKafkaInMemoryIntegrationTest extends AuthorizedSubmodelRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json new file mode 100644 index 000000000..e9c5d704a --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummySubmodelIdShort_3", + "id": "dummySubmodelId_3", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/ZHVtbXlTdWJtb2RlbElkXzM=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json new file mode 100644 index 000000000..370b78932 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificSubmodelIdShort-2", + "id": "specificSubmodelId-2", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/c3BlY2lmaWNTdWJtb2RlbElkLTI=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/modulus.txt b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/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.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/rbac_rules.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/rbac_rules.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/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/pom.xml index 8e9a504f0..e91d80cc8 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/pom.xml @@ -20,6 +20,14 @@ + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx basyx.submodelregistry-service-mongodb-storage diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java new file mode 100644 index 000000000..3585df7c1 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/AuthorizedKafkaMongoDBIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.AuthorizedSubmodelRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Kafka events and MongoDB storage + * + * @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 AuthorizedKafkaMongoDBIntegrationTest extends AuthorizedSubmodelRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json new file mode 100644 index 000000000..e9c5d704a --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummySubmodelIdShort_3", + "id": "dummySubmodelId_3", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/ZHVtbXlTdWJtb2RlbElkXzM=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json new file mode 100644 index 000000000..370b78932 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificSubmodelIdShort-2", + "id": "specificSubmodelId-2", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/c3BlY2lmaWNTdWJtb2RlbElkLTI=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/modulus.txt b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/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.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/rbac_rules.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/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/pom.xml index cc332e06b..5cf7112ad 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/pom.xml @@ -21,6 +21,14 @@ submodel-registry-log-mem + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx @@ -54,5 +62,10 @@ spring-boot-starter-test test + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-service-basetests + test + diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java new file mode 100644 index 000000000..7eeea50d9 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/AuthorizedLogInMemoryIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.tests; + +import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.AuthorizedSubmodelRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Logging events and InMemory storage + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=logEvents,inMemoryStorage", "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"}) +public class AuthorizedLogInMemoryIntegrationTest extends AuthorizedSubmodelRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json new file mode 100644 index 000000000..e9c5d704a --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummySubmodelIdShort_3", + "id": "dummySubmodelId_3", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/ZHVtbXlTdWJtb2RlbElkXzM=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json new file mode 100644 index 000000000..370b78932 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificSubmodelIdShort-2", + "id": "specificSubmodelId-2", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/c3BlY2lmaWNTdWJtb2RlbElkLTI=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/modulus.txt b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/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.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/rbac_rules.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/rbac_rules.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/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/pom.xml index bcc9ef90a..751d571c1 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/pom.xml @@ -20,6 +20,14 @@ submodel-registry-log-mongodb + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + org.eclipse.digitaltwin.basyx basyx.submodelregistry-service-mongodb-storage @@ -44,6 +52,11 @@ org.apache.commons commons-lang3 + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-service-basetests + test + diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/release/log/mongodb/tests/AuthorizedLogMongoDBIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/release/log/mongodb/tests/AuthorizedLogMongoDBIntegrationTest.java new file mode 100644 index 000000000..38e816e77 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/release/log/mongodb/tests/AuthorizedLogMongoDBIntegrationTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.release.log.mongodb.tests; + +import org.eclipse.digitaltwin.basyx.submodelregistry.service.tests.integration.AuthorizedSubmodelRegistryTestSuite; +import org.springframework.test.context.TestPropertySource; + +/** + * Tests for Authorization with Logging events and MongoDB storage + * + * @author danish + */ +@TestPropertySource(properties = {"spring.profiles.active=logEvents,mongoDbStorage", "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 AuthorizedLogMongoDBIntegrationTest extends AuthorizedSubmodelRegistryTestSuite { + +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json new file mode 100644 index 000000000..11e93135d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/HealthOutput.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json new file mode 100644 index 000000000..e9c5d704a --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "dummySubmodelIdShort_3", + "id": "dummySubmodelId_3", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/ZHVtbXlTdWJtb2RlbElkXzM=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json new file mode 100644 index 000000000..370b78932 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -0,0 +1,24 @@ +{ + "description": null, + "displayName": null, + "extensions": null, + "administration": null, + "idShort": "specificSubmodelIdShort-2", + "id": "specificSubmodelId-2", + "semanticId": null, + "supplementalSemanticId": null, + "endpoints": [ + { + "interface": "AAS-3.0", + "protocolInformation": { + "href": "http://127.0.0.1:8080/submodel-descriptors/c3BlY2lmaWNTdWJtb2RlbElkLTI=", + "endpointProtocol": "http", + "endpointProtocolVersion": null, + "subprotocol": null, + "subprotocolBody": null, + "subprotocolBodyEncoding": null, + "securityAttributes": null + } + } + ] +} \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/modulus.txt b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/modulus.txt new file mode 100644 index 000000000..9def9d5fa --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/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.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/rbac_rules.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/rbac_rules.json new file mode 100644 index 000000000..9fcf84488 --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/rbac_rules.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/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/RestConfiguration.java b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/RestConfiguration.java index 48d537c8d..7c8cf2e2b 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/RestConfiguration.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/configuration/RestConfiguration.java @@ -24,8 +24,10 @@ ******************************************************************************/ package org.eclipse.digitaltwin.basyx.submodelregistry.service.configuration; +import org.eclipse.digitaltwin.basyx.http.Aas4JHTTPSerializationExtension; import org.eclipse.digitaltwin.basyx.http.BaSyxHTTPConfiguration; import org.eclipse.digitaltwin.basyx.http.CorsPathPatternProvider; +import org.eclipse.digitaltwin.basyx.http.SerializationExtension; import org.eclipse.digitaltwin.basyx.submodelregistry.service.api.LocationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -62,4 +64,9 @@ public MappingJackson2HttpMessageConverter submodelRegistryMappingJackson2HttpMe public CorsPathPatternProvider getSubmodelRegistryServiceCorsUrlProvider() { return new CorsPathPatternProvider("/submodel-descriptors/**"); } + + @Bean + public SerializationExtension getSubmodelRegistryExtension() { + return new Aas4JHTTPSerializationExtension(); + } } diff --git a/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/errors/BasyxControllerAdvice.java b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/errors/BasyxControllerAdvice.java index 445b0416a..b2a97eb17 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/errors/BasyxControllerAdvice.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/errors/BasyxControllerAdvice.java @@ -26,6 +26,7 @@ import java.time.OffsetDateTime; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; import org.eclipse.digitaltwin.basyx.submodelregistry.model.Message; import org.eclipse.digitaltwin.basyx.submodelregistry.model.Message.MessageTypeEnum; import org.eclipse.digitaltwin.basyx.submodelregistry.model.Result; @@ -61,6 +62,11 @@ public ResponseEntity handleExceptions(Exception ex) { return newResultEntity(ex, HttpStatus.INTERNAL_SERVER_ERROR); } + @ExceptionHandler(InsufficientPermissionException.class) + public ResponseEntity handleInsufficientPermissionException(InsufficientPermissionException exception) { + return newResultEntity(exception, HttpStatus.FORBIDDEN); + } + private ResponseEntity newResultEntity(Exception ex, HttpStatus status) { Result result = new Result(); Message message = newExceptionMessage(ex.getMessage(), status); diff --git a/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/SubmodelRegistryStorageFeature.java b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/SubmodelRegistryStorageFeature.java new file mode 100644 index 000000000..48a81fd1d --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/SubmodelRegistryStorageFeature.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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; + +/** + * Base interface for all features for the SubmodelRegistry + * + * @author danish + */ +public interface SubmodelRegistryStorageFeature { + + SubmodelRegistryStorage decorate(SubmodelRegistryStorage submodelRegistryStorage); + + boolean isEnabled(); + + String getName(); + +} diff --git a/basyx.submodelregistry/basyx.submodelregistry-service/templates/openapi2SpringBoot.mustache b/basyx.submodelregistry/basyx.submodelregistry-service/templates/openapi2SpringBoot.mustache new file mode 100644 index 000000000..3778320ee --- /dev/null +++ b/basyx.submodelregistry/basyx.submodelregistry-service/templates/openapi2SpringBoot.mustache @@ -0,0 +1,34 @@ +package {{basePackage}}; + +{{#openApiNullable}} +import com.fasterxml.jackson.databind.Module; +import org.openapitools.jackson.nullable.JsonNullableModule; +{{/openApiNullable}} +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator; + +@SpringBootApplication( + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +@ComponentScan( + basePackages = {"org.eclipse.digitaltwin.basyx.authorization", "org.eclipse.digitaltwin.basyx.authorization.rbac", "org.eclipse.digitaltwin.basyx.submodelregistry.feature", "{{basePackage}}", "{{apiPackage}}" , "{{configPackage}}"}, + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +public class OpenApiGeneratorApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenApiGeneratorApplication.class, args); + } + +{{#openApiNullable}} + @Bean(name = "{{basePackage}}.OpenApiGeneratorApplication.jsonNullableModule") + public Module jsonNullableModule() { + return new JsonNullableModule(); + } +{{/openApiNullable}} + +} \ No newline at end of file diff --git a/basyx.submodelregistry/pom.xml b/basyx.submodelregistry/pom.xml index c7aba5e6c..86c61f53b 100644 --- a/basyx.submodelregistry/pom.xml +++ b/basyx.submodelregistry/pom.xml @@ -50,6 +50,7 @@ basyx.submodelregistry-service-release-kafka-mem basyx.submodelregistry-service-release-log-mongodb basyx.submodelregistry-service-release-kafka-mongodb + basyx.submodelregistry-feature-authorization @@ -184,6 +185,11 @@ compiler ${mustache.compiler.version} + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + ${project.version} + diff --git a/pom.xml b/pom.xml index bed1e095b..d83311756 100644 --- a/pom.xml +++ b/pom.xml @@ -1016,6 +1016,18 @@ ${revision} tests + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + ${revision} + tests + + + org.eclipse.digitaltwin.basyx + basyx.submodelregistry-feature-authorization + ${revision} + tests + org.eclipse.digitaltwin.basyx basyx.conceptdescriptionrepository.component