From 13251a69b644584520dd09899860f8259807b9c3 Mon Sep 17 00:00:00 2001 From: Mohammad Ghazanfar Ali Danish <62088117+mdanish98@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:05:08 +0200 Subject: [PATCH] Implements a feature to support the Submodel-based RBAC rules backend (also Dynamic RBAC rules management) (#407) * Updates Dynamic RBAC Management Signed-off-by: Mohammad Ghazanfar Ali Danish * Updates SMAuth Rbac Storgae Signed-off-by: Mohammad Ghazanfar Ali Danish * Adds test suite Signed-off-by: Mohammad Ghazanfar Ali Danish * Adds tests Signed-off-by: Mohammad Ghazanfar Ali Danish * Updates tests Signed-off-by: Mohammad Ghazanfar Ali Danish * Adds documentation and fixes RbacRuleDeserializer Signed-off-by: Mohammad Ghazanfar Ali Danish * Adds version to SM Client Signed-off-by: Mohammad Ghazanfar Ali Danish * Cleans up the code Signed-off-by: Mohammad Ghazanfar Ali Danish * Includes AAS Registry Signed-off-by: Mohammad Ghazanfar Ali Danish * Updates documentation for AAS-Registry Signed-off-by: Mohammad Ghazanfar Ali Danish --------- Signed-off-by: Mohammad Ghazanfar Ali Danish --- .../pom.xml | 4 + .../pom.xml | 4 + .../Readme.md | 194 +++++++++++++++++- .../pom.xml | 8 + .../AuthorizedAasRegistryConfiguration.java | 8 + .../AasRegistryTargetInformationAdapter.java | 87 ++++++++ ...sRegistryTargetInformationAdapterTest.java | 143 +++++++++++++ .../Readme.md | 190 ++++++++++++++++- .../pom.xml | 8 + .../AuthorizedAasRepositoryConfiguration.java | 8 + .../submodel/AasTargetInformationAdapter.java | 87 ++++++++ .../AasTargetInformationAdapterTest.java | 142 +++++++++++++ ...MultiUrlAasRepositoryRegistryLinkTest.java | 2 +- .../src/main/resources/application.properties | 7 + .../pom.xml | 27 +++ .../InMemoryAuthorizationRbacStorage.java | 13 +- .../inmemory}/RbacRuleConfiguration.java | 8 +- .../inmemory/InMemoryRbacStorageTest.java | 47 +++++ .../pom.xml | 50 +++++ .../backend/submodel/RbacRuleAdapter.java | 109 ++++++++++ .../submodel/RbacRuleConfiguration.java | 116 +++++++++++ .../SubmodelAuthorizationRbacStorage.java | 105 ++++++++++ .../submodel/TargetInformationAdapter.java | 57 +++++ .../backend/submodel/RbacRuleAdapterTest.java | 183 +++++++++++++++++ .../submodel/SubmodelRbacStorageTest.java | 161 +++++++++++++++ basyx.common/basyx.authorization/pom.xml | 8 +- .../CommonAuthorizationProperties.java | 10 + .../basyx/authorization/rbac/Action.java | 11 +- .../rbac/RbacRuleDeserializer.java | 8 +- .../rbac/RbacRuleKeyGenerator.java | 6 +- .../rbac/TargetInformationTypeProcessor.java | 57 +++++ .../authorization/RbacStorageTestSuite.java | 152 ++++++++++++++ .../InvalidTargetInformationException.java | 45 ++++ basyx.common/pom.xml | 2 + .../pom.xml | 4 + .../pom.xml | 4 + .../pom.xml | 4 + ...UrlSubmodelRepositoryRegistryLinkTest.java | 2 +- ci/docker-compose.yml | 20 ++ .../rules/rbac_rules-conf-sm-repo.json | 155 ++++++++++++++ pom.xml | 22 ++ 41 files changed, 2250 insertions(+), 28 deletions(-) create mode 100644 basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/backend/submodel/AasRegistryTargetInformationAdapter.java create mode 100644 basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/AasRegistryTargetInformationAdapterTest.java create mode 100644 basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/rbac/backend/submodel/AasTargetInformationAdapter.java create mode 100644 basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AasTargetInformationAdapterTest.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.inmemory/pom.xml rename basyx.common/{basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac => basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory}/InMemoryAuthorizationRbacStorage.java (85%) rename basyx.common/{basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac => basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory}/RbacRuleConfiguration.java (86%) create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryRbacStorageTest.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/pom.xml create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapter.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleConfiguration.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/TargetInformationAdapter.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapterTest.java create mode 100644 basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelRbacStorageTest.java create mode 100644 basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/TargetInformationTypeProcessor.java create mode 100644 basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/RbacStorageTestSuite.java create mode 100644 basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/InvalidTargetInformationException.java create mode 100644 ci/keycloak/rules/rbac_rules-conf-sm-repo.json diff --git a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-feature-authorization/pom.xml b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-feature-authorization/pom.xml index 6456cfa3e..1e4ce3d5a 100644 --- a/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-feature-authorization/pom.xml +++ b/basyx.aasdiscoveryservice/basyx.aasdiscoveryservice-feature-authorization/pom.xml @@ -60,5 +60,9 @@ commons-io commons-io + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + \ No newline at end of file diff --git a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/pom.xml b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/pom.xml index 714cac31f..637abee7e 100644 --- a/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/pom.xml +++ b/basyx.aasenvironment/basyx.aasenvironment-feature-authorization/pom.xml @@ -78,5 +78,9 @@ commons-io commons-io + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md b/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md index 653b0523c..799fb5bde 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/Readme.md @@ -28,16 +28,48 @@ spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/real ## RBAC rule configuration -For configuring RBAC rules, all the rbac rules should be configured inside a json file, the rules are defined as below: +This section outlines how RBAC rules are defined, stored, and managed, and provides detailed explanations of the backend persistency options available. +### What is a Rule in RBAC? + +A rule in the context of RBAC is a policy that defines the actions a specific role can perform on certain resources. Each rule typically consists of the following components: + +* **Role**: The entity (user or group) that the rule applies to, such as `admin` or `basyx-reader`. +* **Action**: The operation permitted by the role, such as `CREATE`, `READ`, `UPDATE`, or `DELETE`. +* **Target Information**: The resource(s) that the action can be performed on, like `aas` (Asset Administration Shell) IDs. The targetInformation defines coarse-grained control over the resource, you may define the aasIds with a wildcard (\*), it means the defined role x with action y can access any Asset Administration Shell on the repository. 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. There could be a single aasId or multiple aasIds as a list. + +### Example of a Simple RBAC Rule +```json +{ + "role": "admin", + "action": "READ", + "targetInformation": { + "@type": "aas-registry", + "aasIds": "*" + } +} ``` + +In this example, the rule grants the admin role permission to perform the READ action on all resources of type aas. + +## Persistency Support for RBAC Rules + +The AAS Repository supports two backend persistency mechanisms for storing RBAC rules: + +### InMemory RBAC Storage + +* InMemory RBAC stores all rules directly in the application's memory. +* The rules must be configured during the application startup, and they remain in memory for the duration of the application's runtime. +* Rules are defined using a JSON format as defined below. + +```json [ { "role": "basyx-reader", "action": "READ", "targetInformation": { "@type": "aas-registry", - "aasId": "*" + "aasIds": "*" } }, { @@ -45,7 +77,7 @@ For configuring RBAC rules, all the rbac rules should be configured inside a jso "action": ["CREATE", "READ", "UPDATE", "DELETE"], "targetInformation": { "@type": "aas-registry", - "aasId": "*" + "aasIds": "*" } }, { @@ -53,19 +85,161 @@ For configuring RBAC rules, all the rbac rules should be configured inside a jso "action": "DELETE", "targetInformation": { "@type": "aas-registry", - "aasId": "specificAasId" + "aasIds": ["testAasId1", "specificAasId", "testAasId2"] } } - ] +] +``` + +[!Note] +* The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE, and EXECUTE). +* Rules cannot be modified after the application has started. +* This is suitable for simple, small-scale infrastructure or testing environment where the rule set remains static. + +#### How to enable this storage? + +The InMemory rule storage is used by default, but to explicitly configure, below propertiy needs to be configured inorder to enable the InMemory Storage: + +``` +basyx.feature.authorization.rules.backend=InMemory +``` + +### Submodel-based RBAC Storage + +* RBAC rules are stored in a dedicated Security Submodel within a Configuration-Submodel Repository. +* The Configuration-Submodel Repository is a general-purpose repository that supports various configuration models, with RBAC rules being part of its Security Submodel. +* This repository is equipped with Authorization to ensure that only designated entities (like administrators or maintainers) can manage the rules inside the Security Submodel. +* Similar to InMemory RBAC storage, initial rules can be defined in JSON format. However, upon application startup, these rules are automatically adapted and stored in the Security Submodel. +* This allows for a more flexible and persistent management of rules, which can be updated or extended. + +#### Example Configuration Process: +* Define initial rules in JSON format. (Not mandatory) +* On application startup, these rules are validated and stored in the Security Submodel. + +The below JSON RBAC rule is automatically adapted to Submodel-based RBAC rule: + +```json +{ + "role": "admin", + "action": "DELETE", + "targetInformation": { + "@type": "aas-registry", + "aasIds": "*" + } +} +``` + +The equivalent of above rule in Submodel-based RBAC rule (below in JSON serialized format): +```json +{ + "modelType": "SubmodelElementCollection", + "idShort": "YWRtaW5ERUxFVEVvcmcuZWNsaXBzZS5kaWdpdGFsdHdpbi5iYXN5eC5hYXNyZXBvc2l0b3J5LmZlYXR1cmUuYXV0aG9yaXphdGlvbi5BYXNUYXJnZXRJbmZvcm1hdGlvbg==", + "value": [ + { + "modelType": "Property", + "value": "admin", + "idShort": "role" + }, + { + "modelType": "SubmodelElementList", + "idShort": "action", + "orderRelevant": true, + "value": [ + { + "modelType": "Property", + "value": "DELETE" + } + ] + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "targetInformation", + "value": [ + { + "modelType": "SubmodelElementList", + "idShort": "aasIds", + "orderRelevant": true, + "value": [ + { + "modelType": "Property", + "value": "*" + } + ] + } + ] + } + ] +} +``` + +[!Note] +* The API for adding and removing rules is consistent with that of a standard Submodel Repository. +* The IdShort of the rule is automatically generated and it will replace the original IdShort configured while adding the rule. +* Only addition and removal of rules are supported; updating existing rules is not allowed due to [constraints](#constraints-and-rule-management). +* Only the responsible entity (typically an administrator or maintainer) should be permitted to manage the rules within the Security Submodel. +* This ensures that the RBAC policies are strictly controlled and secure. + +#### How to enable this storage? + +The following properties needs to be configured inorder to enable the Submodel-based Storage: + +``` +basyx.feature.authorization.rules.backend=Submodel +basyx.feature.authorization.rules.backend.submodel.authorization.endpoint= +basyx.feature.authorization.rules.backend.submodel.authorization.token-endpoint= +basyx.feature.authorization.rules.backend.submodel.authorization.grant-type = or +basyx.feature.authorization.rules.backend.submodel.authorization.client-id= +basyx.feature.authorization.rules.backend.submodel.authorization.client-secret= +basyx.feature.authorization.rules.backend.submodel.authorization.username= +basyx.feature.authorization.rules.backend.submodel.authorization.password= ``` -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). +## Constraints and Rule Management -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. +* To ensure quick access to rules, a hash key is generated based on the role, single action, and target information type. +* Duplicate entries with the same role, action, and target information type are not allowed. +* This necessitates the splitting of rules and prevents the update of existing rules (since modifying a rule would alter the hash key, which could impact performance). +* Due to the constraint on hash key generation, updates to existing rules are not supported. +* If a rule needs to be changed, the existing rule must be removed, and a new rule must be added with the updated information. +* Applies to both the storage backend. -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. -* Each rule should be unique in combination of role + action + target information +### Automatic Rule Splitting + +* If a rule specifies multiple actions (e.g., ["CREATE", "READ"]), it will automatically be split into individual rules, each with a single action. + +Example of Automatic Splitting: + +```json +{ + "role": "admin", + "action": ["CREATE", "READ"], + "targetInformation": { + "@type": "aas-registry", + "aasIds": "*" + } +} +``` + +Will be split into: + +```json +{ + "role": "admin", + "action": "CREATE", + "targetInformation": { + "@type": "aas-registry", + "aasIds": "*" + } +}, +{ + "role": "admin", + "action": "READ", + "targetInformation": { + "@type": "aas-registry", + "aasIds": "*" + } +} +``` ## Action table for RBAC diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml b/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml index 55bc2538f..be57092e4 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/pom.xml @@ -57,5 +57,13 @@ basyx.aasregistry-service-inmemory-storage test + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.submodel + 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 index 8c34ce5f7..b59a03471 100644 --- 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 @@ -26,12 +26,14 @@ package org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization; import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.rbac.AasRegistryTargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.rbac.backend.submodel.AasRegistryTargetInformationAdapter; 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.authorization.rules.rbac.backend.submodel.TargetInformationAdapter; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,5 +56,11 @@ public TargetPermissionVerifier getAasTargetPermis public RbacPermissionResolver getAasPermissionResolver(RbacStorage rbacStorage, RoleProvider roleProvider, TargetPermissionVerifier targetPermissionVerifier) { return new SimpleRbacPermissionResolver<>(rbacStorage, roleProvider, targetPermissionVerifier); } + + @Bean + public TargetInformationAdapter getAasRegistryTargetInformationAdapter() { + + return new AasRegistryTargetInformationAdapter(); + } } diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/backend/submodel/AasRegistryTargetInformationAdapter.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/backend/submodel/AasRegistryTargetInformationAdapter.java new file mode 100644 index 000000000..ee37eaadf --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/authorization/rbac/backend/submodel/AasRegistryTargetInformationAdapter.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * 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.backend.submodel; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.AasRegistryTargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rules.rbac.backend.submodel.TargetInformationAdapter; +import org.eclipse.digitaltwin.basyx.core.exceptions.InvalidTargetInformationException; + +/** + * An implementation of the {@link TargetInformationAdapter} to adapt with Aas + * {@link TargetInformation} + * + * @author danish + */ +public class AasRegistryTargetInformationAdapter implements TargetInformationAdapter { + + @Override + public SubmodelElementCollection adapt(TargetInformation targetInformation) { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation").build(); + + SubmodelElementList aasId = new DefaultSubmodelElementList.Builder().idShort("aasIds").build(); + + List aasIds = ((AasRegistryTargetInformation) targetInformation).getAasIds().stream().map(this::transform).collect(Collectors.toList()); + aasId.setValue(aasIds); + + targetInformationSMC.setValue(Arrays.asList(aasId)); + + return targetInformationSMC; + } + + @Override + public TargetInformation adapt(SubmodelElementCollection targetInformation) { + + SubmodelElement aasIdSubmodelElement = targetInformation.getValue().stream().filter(sme -> sme.getIdShort().equals("aasIds")).findAny().orElseThrow( + () -> new InvalidTargetInformationException("The TargetInformation defined in the SubmodelElementCollection Rule with id: " + targetInformation.getIdShort() + " is not compatible with the " + getClass().getName())); + + if (!(aasIdSubmodelElement instanceof SubmodelElementList)) + throw new InvalidTargetInformationException("The TargetInformation defined in the SubmodelElementCollection Rule with id: " + targetInformation.getIdShort() + " is not compatible with the " + getClass().getName()); + + SubmodelElementList aasIdList = (SubmodelElementList) aasIdSubmodelElement; + + List aasIds = aasIdList.getValue().stream().map(Property.class::cast).map(Property::getValue).collect(Collectors.toList()); + + return new AasRegistryTargetInformation(aasIds); + } + + private Property transform(String aasId) { + return new DefaultProperty.Builder().value(aasId).build(); + } + +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/AasRegistryTargetInformationAdapterTest.java b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/AasRegistryTargetInformationAdapterTest.java new file mode 100644 index 000000000..d3bac91be --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/regression/feature/authorization/AasRegistryTargetInformationAdapterTest.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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 static org.junit.Assert.*; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.AasRegistryTargetInformation; +import org.eclipse.digitaltwin.basyx.aasregistry.feature.authorization.rbac.backend.submodel.AasRegistryTargetInformationAdapter; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.core.exceptions.InvalidTargetInformationException; + +/** + * Tests {@link AasRegistryTargetInformationAdapter} + * + * @author danish + */ +public class AasRegistryTargetInformationAdapterTest { + + private AasRegistryTargetInformationAdapter aasRegistryTargetInformationAdapter; + + @Before + public void setUp() { + aasRegistryTargetInformationAdapter = new AasRegistryTargetInformationAdapter(); + } + + @Test + public void testAdaptTargetInformationToSubmodelElementCollection() { + + List aasIds = Arrays.asList("aasId1", "aasId2"); + TargetInformation targetInformation = new AasRegistryTargetInformation(aasIds); + + SubmodelElementCollection result = aasRegistryTargetInformationAdapter.adapt(targetInformation); + + assertEquals("targetInformation", result.getIdShort()); + + List elements = result.getValue(); + assertEquals(1, elements.size()); + + SubmodelElementList aasIdList = (SubmodelElementList) elements.get(0); + assertEquals("aasIds", aasIdList.getIdShort()); + + List actualAasIds = aasIdList.getValue().stream().map(Property.class::cast).map(Property::getValue).map(String::valueOf).collect(Collectors.toList()); + assertEquals(aasIds, actualAasIds); + } + + @Test + public void testAdaptSubmodelElementCollectionToTargetInformation() { + + List expectedAasIds = Arrays.asList("aasId1", "aasId2"); + List aasIdProperties = expectedAasIds.stream().map(aasId -> new DefaultProperty.Builder().value(aasId).build()).collect(Collectors.toList()); + + SubmodelElementList aasIdList = new DefaultSubmodelElementList.Builder().idShort("aasIds").value(aasIdProperties).build(); + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation").value(Collections.singletonList(aasIdList)).build(); + + TargetInformation result = aasRegistryTargetInformationAdapter.adapt(targetInformationSMC); + + assertTrue(result instanceof AasRegistryTargetInformation); + assertEquals(expectedAasIds, ((AasRegistryTargetInformation) result).getAasIds()); + } + + @Test + public void testAdaptTargetInformationWithEmptyAasIds() { + + List aasIds = Collections.emptyList(); + TargetInformation targetInformation = new AasRegistryTargetInformation(aasIds); + + SubmodelElementCollection result = aasRegistryTargetInformationAdapter.adapt(targetInformation); + + assertEquals("targetInformation", result.getIdShort()); + + List elements = result.getValue(); + assertEquals(1, elements.size()); + + SubmodelElementList aasIdList = (SubmodelElementList) elements.get(0); + assertEquals("aasIds", aasIdList.getIdShort()); + + List actualAasIds = aasIdList.getValue().stream() + .map(Property.class::cast) + .map(Property::getValue) + .map(String::valueOf) + .collect(Collectors.toList()); + assertTrue(actualAasIds.isEmpty()); + } + + @Test(expected = InvalidTargetInformationException.class) + public void testAdaptSubmodelElementCollectionWithInvalidStructure() { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation") + .value(Collections.singletonList(new DefaultProperty.Builder().idShort("invalidElement").value("value").build())) + .build(); + + aasRegistryTargetInformationAdapter.adapt(targetInformationSMC); + } + + @Test(expected = InvalidTargetInformationException.class) + public void testAdaptSubmodelElementCollectionWithoutAasIds() { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation") + .value(Collections.emptyList()) + .build(); + + aasRegistryTargetInformationAdapter.adapt(targetInformationSMC); + } + +} diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/Readme.md b/basyx.aasrepository/basyx.aasrepository-feature-authorization/Readme.md index 25054a51b..5a1fac1c8 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-authorization/Readme.md +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/Readme.md @@ -7,7 +7,7 @@ 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 = +basyx.feature.authorization.rbac.file = spring.security.oauth2.resourceserver.jwt.issuer-uri= ``` @@ -28,9 +28,41 @@ spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/real ## RBAC rule configuration -For configuring RBAC rules, all the rbac rules should be configured inside a json file, the rules are defined as below: +This section outlines how RBAC rules are defined, stored, and managed, and provides detailed explanations of the backend persistency options available. +### What is a Rule in RBAC? + +A rule in the context of RBAC is a policy that defines the actions a specific role can perform on certain resources. Each rule typically consists of the following components: + +* **Role**: The entity (user or group) that the rule applies to, such as `admin` or `basyx-reader`. +* **Action**: The operation permitted by the role, such as `CREATE`, `READ`, `UPDATE`, or `DELETE`. +* **Target Information**: The resource(s) that the action can be performed on, like `aas` (Asset Administration Shell) IDs. The targetInformation defines coarse-grained control over the resource, you may define the aasIds with a wildcard (\*), it means the defined role x with action y can access any Asset Administration Shell on the repository. 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. There could be a single aasId or multiple aasIds as a list. + +### Example of a Simple RBAC Rule +```json +{ + "role": "admin", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } +} ``` + +In this example, the rule grants the admin role permission to perform the READ action on all resources of type aas. + +## Persistency Support for RBAC Rules + +The AAS Repository supports two backend persistency mechanisms for storing RBAC rules: + +### InMemory RBAC Storage + +* InMemory RBAC stores all rules directly in the application's memory. +* The rules must be configured during the application startup, and they remain in memory for the duration of the application's runtime. +* Rules are defined using a JSON format as defined below. + +```json [ { "role": "basyx-reader", @@ -56,16 +88,158 @@ For configuring RBAC rules, all the rbac rules should be configured inside a jso "aasIds": ["testAasId1", "specificAasId", "testAasId2"] } } - ] +] ``` -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). +[!Note] +* The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE, and EXECUTE). +* Rules cannot be modified after the application has started. +* This is suitable for simple, small-scale infrastructure or testing environment where the rule set remains static. -The targetInformation defines coarse-grained control over the resource, you may define the aasIds with a wildcard (\*), it means the defined role x with action y can access any Asset Administration Shell on the repository. 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. There could be a single aasId or multiple aasIds as a list (cf. basyx-deleter above). +#### How to enable this storage? -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. -* Each rule should be unique in combination of role + action + target information +The InMemory rule storage is used by default, but to explicitly configure, below propertiy needs to be configured inorder to enable the InMemory Storage: + +``` +basyx.feature.authorization.rules.backend=InMemory +``` + +### Submodel-based RBAC Storage + +* RBAC rules are stored in a dedicated Security Submodel within a Configuration-Submodel Repository. +* The Configuration-Submodel Repository is a general-purpose repository that supports various configuration models, with RBAC rules being part of its Security Submodel. +* This repository is equipped with Authorization to ensure that only designated entities (like administrators or maintainers) can manage the rules inside the Security Submodel. +* Similar to InMemory RBAC storage, initial rules can be defined in JSON format. However, upon application startup, these rules are automatically adapted and stored in the Security Submodel. +* This allows for a more flexible and persistent management of rules, which can be updated or extended. + +#### Example Configuration Process: +* Define initial rules in JSON format. (Not mandatory) +* On application startup, these rules are validated and stored in the Security Submodel. + +The below JSON RBAC rule is automatically adapted to Submodel-based RBAC rule: + +```json +{ + "role": "admin", + "action": "DELETE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } +} +``` + +The equivalent of above rule in Submodel-based RBAC rule (below in JSON serialized format): +```json +{ + "modelType": "SubmodelElementCollection", + "idShort": "YWRtaW5ERUxFVEVvcmcuZWNsaXBzZS5kaWdpdGFsdHdpbi5iYXN5eC5hYXNyZXBvc2l0b3J5LmZlYXR1cmUuYXV0aG9yaXphdGlvbi5BYXNUYXJnZXRJbmZvcm1hdGlvbg==", + "value": [ + { + "modelType": "Property", + "value": "admin", + "idShort": "role" + }, + { + "modelType": "SubmodelElementList", + "idShort": "action", + "orderRelevant": true, + "value": [ + { + "modelType": "Property", + "value": "DELETE" + } + ] + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "targetInformation", + "value": [ + { + "modelType": "SubmodelElementList", + "idShort": "aasIds", + "orderRelevant": true, + "value": [ + { + "modelType": "Property", + "value": "*" + } + ] + } + ] + } + ] +} +``` + +[!Note] +* The API for adding and removing rules is consistent with that of a standard Submodel Repository. +* The IdShort of the rule is automatically generated and it will replace the original IdShort configured while adding the rule. +* Only addition and removal of rules are supported; updating existing rules is not allowed due to [constraints](#constraints-and-rule-management). +* Only the responsible entity (typically an administrator or maintainer) should be permitted to manage the rules within the Security Submodel. +* This ensures that the RBAC policies are strictly controlled and secure. + +#### How to enable this storage? + +The following properties needs to be configured inorder to enable the Submodel-based Storage: + +``` +basyx.feature.authorization.rules.backend=Submodel +basyx.feature.authorization.rules.backend.submodel.authorization.endpoint= +basyx.feature.authorization.rules.backend.submodel.authorization.token-endpoint= +basyx.feature.authorization.rules.backend.submodel.authorization.grant-type = or +basyx.feature.authorization.rules.backend.submodel.authorization.client-id= +basyx.feature.authorization.rules.backend.submodel.authorization.client-secret= +basyx.feature.authorization.rules.backend.submodel.authorization.username= +basyx.feature.authorization.rules.backend.submodel.authorization.password= +``` + +## Constraints and Rule Management + +* To ensure quick access to rules, a hash key is generated based on the role, single action, and target information type. +* Duplicate entries with the same role, action, and target information type are not allowed. +* This necessitates the splitting of rules and prevents the update of existing rules (since modifying a rule would alter the hash key, which could impact performance). +* Due to the constraint on hash key generation, updates to existing rules are not supported. +* If a rule needs to be changed, the existing rule must be removed, and a new rule must be added with the updated information. +* Applies to both the storage backend. + +### Automatic Rule Splitting + +* If a rule specifies multiple actions (e.g., ["CREATE", "READ"]), it will automatically be split into individual rules, each with a single action. + +Example of Automatic Splitting: + +```json +{ + "role": "admin", + "action": ["CREATE", "READ"], + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } +} +``` + +Will be split into: + +```json +{ + "role": "admin", + "action": "CREATE", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } +}, +{ + "role": "admin", + "action": "READ", + "targetInformation": { + "@type": "aas", + "aasIds": "*" + } +} +``` ## Action table for RBAC diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/pom.xml b/basyx.aasrepository/basyx.aasrepository-feature-authorization/pom.xml index 15e7d1143..ce679139c 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-authorization/pom.xml +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/pom.xml @@ -32,6 +32,14 @@ org.eclipse.digitaltwin.basyx basyx.authorization + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.submodel + org.eclipse.digitaltwin.basyx basyx.authorization diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AuthorizedAasRepositoryConfiguration.java b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AuthorizedAasRepositoryConfiguration.java index ea6a65a48..b665a510a 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AuthorizedAasRepositoryConfiguration.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AuthorizedAasRepositoryConfiguration.java @@ -26,12 +26,14 @@ package org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization; import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.rbac.AasTargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.rbac.backend.submodel.AasTargetInformationAdapter; 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.SimpleRbacPermissionResolver; import org.eclipse.digitaltwin.basyx.authorization.rbac.RoleProvider; import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetPermissionVerifier; +import org.eclipse.digitaltwin.basyx.authorization.rules.rbac.backend.submodel.TargetInformationAdapter; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,5 +57,11 @@ public RbacPermissionResolver getAasPermissionResolver(Rba return new SimpleRbacPermissionResolver<>(rbacStorage, roleProvider, targetPermissionVerifier); } + + @Bean + public TargetInformationAdapter getAasTargetInformationAdapter() { + + return new AasTargetInformationAdapter(); + } } diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/rbac/backend/submodel/AasTargetInformationAdapter.java b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/rbac/backend/submodel/AasTargetInformationAdapter.java new file mode 100644 index 000000000..1ec0e8fb2 --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/rbac/backend/submodel/AasTargetInformationAdapter.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.rbac.backend.submodel; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.AasTargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.authorization.rules.rbac.backend.submodel.TargetInformationAdapter; +import org.eclipse.digitaltwin.basyx.core.exceptions.InvalidTargetInformationException; + +/** + * An implementation of the {@link TargetInformationAdapter} to adapt with Aas + * {@link TargetInformation} + * + * @author danish + */ +public class AasTargetInformationAdapter implements TargetInformationAdapter { + + @Override + public SubmodelElementCollection adapt(TargetInformation targetInformation) { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation").build(); + + SubmodelElementList aasId = new DefaultSubmodelElementList.Builder().idShort("aasIds").build(); + + List aasIds = ((AasTargetInformation) targetInformation).getAasIds().stream().map(this::transform).collect(Collectors.toList()); + aasId.setValue(aasIds); + + targetInformationSMC.setValue(Arrays.asList(aasId)); + + return targetInformationSMC; + } + + @Override + public TargetInformation adapt(SubmodelElementCollection targetInformation) { + + SubmodelElement aasIdSubmodelElement = targetInformation.getValue().stream().filter(sme -> sme.getIdShort().equals("aasIds")).findAny().orElseThrow( + () -> new InvalidTargetInformationException("The TargetInformation defined in the SubmodelElementCollection Rule with id: " + targetInformation.getIdShort() + " is not compatible with the " + getClass().getName())); + + if (!(aasIdSubmodelElement instanceof SubmodelElementList)) + throw new InvalidTargetInformationException("The TargetInformation defined in the SubmodelElementCollection Rule with id: " + targetInformation.getIdShort() + " is not compatible with the " + getClass().getName()); + + SubmodelElementList aasIdList = (SubmodelElementList) aasIdSubmodelElement; + + List aasIds = aasIdList.getValue().stream().map(Property.class::cast).map(Property::getValue).collect(Collectors.toList()); + + return new AasTargetInformation(aasIds); + } + + private Property transform(String aasId) { + return new DefaultProperty.Builder().value(aasId).build(); + } + +} diff --git a/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AasTargetInformationAdapterTest.java b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AasTargetInformationAdapterTest.java new file mode 100644 index 000000000..b7ed798d5 --- /dev/null +++ b/basyx.aasrepository/basyx.aasrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/authorization/AasTargetInformationAdapterTest.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization; + +import static org.junit.Assert.*; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.eclipse.digitaltwin.basyx.aasrepository.feature.authorization.rbac.backend.submodel.AasTargetInformationAdapter; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.core.exceptions.InvalidTargetInformationException; + +/** + * Tests {@link AasTargetInformationAdapter} + * + * @author danish + */ +public class AasTargetInformationAdapterTest { + + private AasTargetInformationAdapter aasTargetInformationAdapter; + + @Before + public void setUp() { + aasTargetInformationAdapter = new AasTargetInformationAdapter(); + } + + @Test + public void testAdaptTargetInformationToSubmodelElementCollection() { + + List aasIds = Arrays.asList("aasId1", "aasId2"); + TargetInformation targetInformation = new AasTargetInformation(aasIds); + + SubmodelElementCollection result = aasTargetInformationAdapter.adapt(targetInformation); + + assertEquals("targetInformation", result.getIdShort()); + + List elements = result.getValue(); + assertEquals(1, elements.size()); + + SubmodelElementList aasIdList = (SubmodelElementList) elements.get(0); + assertEquals("aasIds", aasIdList.getIdShort()); + + List actualAasIds = aasIdList.getValue().stream().map(Property.class::cast).map(Property::getValue).map(String::valueOf).collect(Collectors.toList()); + assertEquals(aasIds, actualAasIds); + } + + @Test + public void testAdaptSubmodelElementCollectionToTargetInformation() { + + List expectedAasIds = Arrays.asList("aasId1", "aasId2"); + List aasIdProperties = expectedAasIds.stream().map(aasId -> new DefaultProperty.Builder().value(aasId).build()).collect(Collectors.toList()); + + SubmodelElementList aasIdList = new DefaultSubmodelElementList.Builder().idShort("aasIds").value(aasIdProperties).build(); + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation").value(Collections.singletonList(aasIdList)).build(); + + TargetInformation result = aasTargetInformationAdapter.adapt(targetInformationSMC); + + assertTrue(result instanceof AasTargetInformation); + assertEquals(expectedAasIds, ((AasTargetInformation) result).getAasIds()); + } + + @Test + public void testAdaptTargetInformationWithEmptyAasIds() { + + List aasIds = Collections.emptyList(); + TargetInformation targetInformation = new AasTargetInformation(aasIds); + + SubmodelElementCollection result = aasTargetInformationAdapter.adapt(targetInformation); + + assertEquals("targetInformation", result.getIdShort()); + + List elements = result.getValue(); + assertEquals(1, elements.size()); + + SubmodelElementList aasIdList = (SubmodelElementList) elements.get(0); + assertEquals("aasIds", aasIdList.getIdShort()); + + List actualAasIds = aasIdList.getValue().stream() + .map(Property.class::cast) + .map(Property::getValue) + .map(String::valueOf) + .collect(Collectors.toList()); + assertTrue(actualAasIds.isEmpty()); + } + + @Test(expected = InvalidTargetInformationException.class) + public void testAdaptSubmodelElementCollectionWithInvalidStructure() { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation") + .value(Collections.singletonList(new DefaultProperty.Builder().idShort("invalidElement").value("value").build())) + .build(); + + aasTargetInformationAdapter.adapt(targetInformationSMC); + } + + @Test(expected = InvalidTargetInformationException.class) + public void testAdaptSubmodelElementCollectionWithoutAasIds() { + + SubmodelElementCollection targetInformationSMC = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation") + .value(Collections.emptyList()) + .build(); + + aasTargetInformationAdapter.adapt(targetInformationSMC); + } + +} diff --git a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/MultiUrlAasRepositoryRegistryLinkTest.java b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/MultiUrlAasRepositoryRegistryLinkTest.java index ef9ac377d..df660d5cb 100644 --- a/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/MultiUrlAasRepositoryRegistryLinkTest.java +++ b/basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/MultiUrlAasRepositoryRegistryLinkTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2023 the Eclipse BaSyx Authors + * 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 diff --git a/basyx.aasrepository/basyx.aasrepository.component/src/main/resources/application.properties b/basyx.aasrepository/basyx.aasrepository.component/src/main/resources/application.properties index 98cbe003f..6fb440a75 100644 --- a/basyx.aasrepository/basyx.aasrepository.component/src/main/resources/application.properties +++ b/basyx.aasrepository/basyx.aasrepository.component/src/main/resources/application.properties @@ -34,3 +34,10 @@ basyx.backend = InMemory #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 +# +#basyx.feature.authorization.rules.backend=Submodel +#basyx.feature.authorization.rules.backend.submodel.authorization.endpoint=http://localhost:8061/submodels/N0E3MTA0QkRBQjU3RTE4NA== +#basyx.feature.authorization.rules.backend.submodel.authorization.token-endpoint=http://localhost:9097/realms/BaSyx/protocol/openid-connect/token +#basyx.feature.authorization.rules.backend.submodel.authorization.grant-type = CLIENT_CREDENTIALS +#basyx.feature.authorization.rules.backend.submodel.authorization.client-id=workstation-1 +#basyx.feature.authorization.rules.backend.submodel.authorization.client-secret=nY0mjyECF60DGzNmQUjL81XurSl8etom \ No newline at end of file diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/pom.xml b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/pom.xml new file mode 100644 index 000000000..fc5f350da --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.common + ${revision} + + basyx.authorization.rules.rbac.backend.inmemory + + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/InMemoryAuthorizationRbacStorage.java b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryAuthorizationRbacStorage.java similarity index 85% rename from basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/InMemoryAuthorizationRbacStorage.java rename to basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryAuthorizationRbacStorage.java index 49a1c65eb..80d6da9b6 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/InMemoryAuthorizationRbacStorage.java +++ b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryAuthorizationRbacStorage.java @@ -23,10 +23,14 @@ * SPDX-License-Identifier: MIT ******************************************************************************/ -package org.eclipse.digitaltwin.basyx.authorization.rbac; +package org.eclipse.digitaltwin.basyx.authorization.rules.rbac.backend.inmemory; import java.util.Map; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleKeyGenerator; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; + /** * InMemory implementation of the {@link RbacStorage} * @@ -39,16 +43,20 @@ public InMemoryAuthorizationRbacStorage(Map rbacRules) { this.rbacRules = rbacRules; } + @Override public Map getRbacRules() { return rbacRules; } + @Override public void addRule(RbacRule rbacRule) { - rbacRule.getAction().stream().map(action -> RbacRuleKeyGenerator.generateKey(rbacRule.getRole(), action.toString(), rbacRule.getTargetInformation().getClass().getName())).filter(key -> !rbacRules.containsKey(key)).map(key -> rbacRules.put(key, rbacRule)); + rbacRule.getAction().stream().map(action -> RbacRuleKeyGenerator.generateKey(rbacRule.getRole(), action.toString(), rbacRule.getTargetInformation().getClass().getName())).filter(key -> !rbacRules.containsKey(key)).forEach(key -> rbacRules.put(key, rbacRule)); } + @Override public void removeRule(String key) { + if (!exist(key)) throw new RuntimeException("Rule doesn't exist in policy store"); @@ -57,6 +65,7 @@ public void removeRule(String key) { @Override public RbacRule getRbacRule(String key) { + if (!exist(key)) throw new RuntimeException("Rule doesn't exist in policy store"); diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleConfiguration.java b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/RbacRuleConfiguration.java similarity index 86% rename from basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleConfiguration.java rename to basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/RbacRuleConfiguration.java index 0bf358e2f..b15ad95c0 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleConfiguration.java +++ b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/RbacRuleConfiguration.java @@ -23,7 +23,7 @@ * SPDX-License-Identifier: MIT ******************************************************************************/ -package org.eclipse.digitaltwin.basyx.authorization.rbac; +package org.eclipse.digitaltwin.basyx.authorization.rules.rbac.backend.inmemory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -32,6 +32,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ResourceLoader; import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleInitializer; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; + import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.HashMap; @@ -43,7 +47,7 @@ */ @Configuration @ConditionalOnProperty("basyx.feature.authorization.enabled") -@ConditionalOnExpression(value = "'${basyx.feature.authorization.type}' == 'rbac'") +@ConditionalOnExpression(value = "'${basyx.feature.authorization.type}' == 'rbac' && ('${basyx.feature.authorization.rules.backend:InMemory}' == 'InMemory' || '${basyx.feature.authorization.rules.backend}' == '')") public class RbacRuleConfiguration { public static final String RULES_FILE_KEY = "basyx.aasrepository.feature.authorization.rbac.file"; diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryRbacStorageTest.java b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryRbacStorageTest.java new file mode 100644 index 000000000..00fd4aabe --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.inmemory/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/inmemory/InMemoryRbacStorageTest.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.inmemory; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.digitaltwin.basyx.authorization.RbacStorageTestSuite; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; + +/** + * Tests {@link InMemoryAuthorizationRbacStorage} + * + * @author danish + */ +public class InMemoryRbacStorageTest extends RbacStorageTestSuite { + + @Override + protected void setUpRbacStorage() { + Map initialRules = new HashMap<>(); + rbacStorage = new InMemoryAuthorizationRbacStorage(initialRules); + } + +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/pom.xml b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/pom.xml new file mode 100644 index 000000000..1e4c5a114 --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.common + ${revision} + + basyx.authorization.rules.rbac.backend.submodel + + + + org.eclipse.digitaltwin.basyx + basyx.authorization + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-client + 2.0.0-milestone-03.1 + + + org.eclipse.digitaltwin.basyx + basyx.submodelrepository-core + 2.0.0-milestone-03.1 + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-core + 2.0.0-milestone-03.1 + + + org.eclipse.digitaltwin.basyx + basyx.submodelservice-client + 2.0.0-milestone-03.1 + + + org.eclipse.digitaltwin.basyx + basyx.authorization + test + tests + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapter.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapter.java new file mode 100644 index 000000000..aef9f9d7b --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapter.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; + +/** + * + * An interface for adapting {@link RbacRule} to its + * {@link SubmodelElementCollection} equivalent and vice-versa. + * + * @author danish + * + */ +public class RbacRuleAdapter { + + private TargetInformationAdapter targetInformationAdapter; + + public RbacRuleAdapter(TargetInformationAdapter targetInformationAdapter) { + this.targetInformationAdapter = targetInformationAdapter; + } + + /** + * Adapts the {@link RbacRule} into {@link SubmodelElementCollection} rbac rule + * + * @param rbacRule + * @param rbacRuleKey + * @return rbacRule SMC + */ + public SubmodelElementCollection adapt(RbacRule rbacRule, String rbacRuleKey) { + + SubmodelElementCollection rule = new DefaultSubmodelElementCollection.Builder().idShort(rbacRuleKey).build(); + + Property role = new DefaultProperty.Builder().idShort("role").value(rbacRule.getRole()).build(); + SubmodelElementList action = new DefaultSubmodelElementList.Builder().idShort("action").build(); + + List actions = rbacRule.getAction().stream().map(this::transform).collect(Collectors.toList()); + action.setValue(actions); + + SubmodelElementCollection targetInformation = targetInformationAdapter.adapt(rbacRule.getTargetInformation()); + + rule.setValue(Arrays.asList(role, action, targetInformation)); + + return rule; + } + + /** + * Adapts the {@link SubmodelElementCollection} rbac rule into {@link RbacRule} + * + * @param rbacRule + * SMC + * @return rbacRule + */ + public RbacRule adapt(SubmodelElementCollection rbacRule) { + + Property role = (Property) rbacRule.getValue().stream().filter(sme -> sme.getIdShort().equals("role")).findAny().get(); + + SubmodelElementList actionSML = (SubmodelElementList) rbacRule.getValue().stream().filter(sme -> sme.getIdShort().equals("action")).findAny().get(); + + List actions = actionSML.getValue().stream().map(Property.class::cast).map(actionProperty -> actionProperty.getValue()).map(Action::fromString).collect(Collectors.toList()); + + SubmodelElementCollection targetInformationSMC = (SubmodelElementCollection) rbacRule.getValue().stream().filter(sme -> sme.getIdShort().equals("targetInformation")).findAny().get(); + + TargetInformation targetInformation = targetInformationAdapter.adapt(targetInformationSMC); + + return new RbacRule(role.getValue(), actions, targetInformation); + } + + private Property transform(Action action) { + return new DefaultProperty.Builder().value(action.toString()).build(); + } + +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleConfiguration.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleConfiguration.java new file mode 100644 index 000000000..0175a5bd7 --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleConfiguration.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleInitializer; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.AccessTokenProviderFactory; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.AccessTokenProvider; +import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.GrantType; +import org.eclipse.digitaltwin.basyx.submodelservice.client.AuthorizedConnectedSubmodelService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +/** + * Configurations for {@link RbacRule} + * + * @author danish + */ +@Configuration +@ConditionalOnProperty("basyx.feature.authorization.enabled") +@ConditionalOnExpression(value = "'${basyx.feature.authorization.type}' == 'rbac' && '${basyx.feature.authorization.rules.backend}' == 'Submodel'") +public class RbacRuleConfiguration { + public static final String RULES_FILE_KEY = "basyx.aasrepository.feature.authorization.rbac.file"; + + @Value("${" + CommonAuthorizationProperties.RBAC_FILE_PROPERTY_KEY + ":}") + private String filePath; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_SUBMODEL_ENDPOINT + ":}") + private String submodelEndpoint; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_TOKEN_ENDPOINT + ":}") + private String tokenEndpoint; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_GRANT_TYPE + ":}") + private String grantType; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_CLIENT_ID + ":}") + private String clientId; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_CLIENT_SECRET + ":}") + private String clientSecret; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_USERNAME + ":}") + private String username; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_PASSWORD + ":}") + private String password; + + @Value("${" + CommonAuthorizationProperties.RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_SCOPES + ":}") + private List scopes; + + private ObjectMapper objectMapper; + private ResourceLoader resourceLoader; + private TargetInformationAdapter targetInformationAdapter; + + public RbacRuleConfiguration(ObjectMapper objectMapper, ResourceLoader resourceLoader, TargetInformationAdapter targetInformationAdapter) { + this.objectMapper = objectMapper; + this.resourceLoader = resourceLoader; + this.targetInformationAdapter = targetInformationAdapter; + } + + @Bean + public RbacStorage createInMemoryRbacStorage() throws IOException { + + TokenManager tokenManager = new TokenManager(tokenEndpoint, getTokenProvider()); + + if (filePath.isBlank()) + return new SubmodelAuthorizationRbacStorage(new AuthorizedConnectedSubmodelService(submodelEndpoint, tokenManager), new HashMap<>(), new RbacRuleAdapter(targetInformationAdapter)); + + HashMap initialRules = new RbacRuleInitializer(objectMapper, filePath, resourceLoader).deserialize(); + + return new SubmodelAuthorizationRbacStorage(new AuthorizedConnectedSubmodelService(submodelEndpoint, tokenManager), initialRules, new RbacRuleAdapter(targetInformationAdapter)); + } + + private AccessTokenProvider getTokenProvider() { + AccessTokenProviderFactory factory = new AccessTokenProviderFactory(GrantType.valueOf(grantType), scopes); + factory.setClientCredentials(clientId, clientSecret); + factory.setPasswordCredentials(username, password); + return factory.create(); + } + +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java new file mode 100644 index 000000000..fed4005aa --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelAuthorizationRbacStorage.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleKeyGenerator; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; + +/** + * InMemory implementation of the {@link RbacStorage} + * + * @author danish + */ +public class SubmodelAuthorizationRbacStorage implements RbacStorage { + private RbacRuleAdapter ruleAdapter; + private ConnectedSubmodelService smService; + + public SubmodelAuthorizationRbacStorage(ConnectedSubmodelService smService, HashMap initialRules, RbacRuleAdapter ruleAdapter) { + this.ruleAdapter = ruleAdapter; + this.smService = smService; + + initializeRbacRules(initialRules); + } + + @Override + public void addRule(RbacRule rbacRule) { + + List rbacRulesSMC = rbacRule.getAction().stream().map(action -> new RbacRule(rbacRule.getRole(), Arrays.asList(action), rbacRule.getTargetInformation())) + .map(rule -> ruleAdapter.adapt(rule, createKey(rule))).collect(Collectors.toList()); + + rbacRulesSMC.stream().forEach(rule -> smService.createSubmodelElement(rule)); + } + + @Override + public RbacRule getRbacRule(String key) { + + SubmodelElementCollection ruleSMC = (SubmodelElementCollection) smService.getSubmodelElement(key); + + return ruleAdapter.adapt(ruleSMC); + } + + @Override + public void removeRule(String key) { + smService.deleteSubmodelElement(key); + } + + @Override + public boolean exist(String key) { + + try { + smService.getSubmodelElement(key); + return true; + } catch (Exception e) { + return false; + } + + } + + @Override + public Map getRbacRules() { + + return smService.getSubmodelElements(new PaginationInfo(0, "")).getResult().stream().map(SubmodelElementCollection.class::cast).map(ruleAdapter::adapt).collect(Collectors.toMap(rbacRule -> createKey(rbacRule), rbacRule -> rbacRule)); + } + + private String createKey(RbacRule rbacRule) { + + return RbacRuleKeyGenerator.generateKey(rbacRule.getRole(), rbacRule.getAction().get(0).toString(), rbacRule.getTargetInformation().getClass().getName()); + } + + private void initializeRbacRules(HashMap initialRules) { + initialRules.values().stream().forEach(rule -> addRule(rule)); + } +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/TargetInformationAdapter.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/TargetInformationAdapter.java new file mode 100644 index 000000000..7f4090caa --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/TargetInformationAdapter.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; + +/** + * + * An interface for adapting {@link TargetInformation} to its + * {@link SubmodelElementCollection} equivalent and vice-versa. + * + * @author danish + * + */ +public interface TargetInformationAdapter { + + /** + * Adapts the Target information into {@link SubmodelElementCollection} target information + * + * @param targetInformation + * @return targetInformation SMC + */ + SubmodelElementCollection adapt(TargetInformation targetInformation); + + /** + * Adapts the {@link SubmodelElementCollection} target information into {@link TargetInformation} + * + * @param targetInformation SMC + * @return targetInformation + */ + TargetInformation adapt(SubmodelElementCollection targetInformation); + +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapterTest.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapterTest.java new file mode 100644 index 000000000..1c125f4f0 --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/RbacRuleAdapterTest.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.*; + +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleKeyGenerator; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Tests {@link RbacRuleAdapterTest} + * + * @author danish + */ +public class RbacRuleAdapterTest { + + @Mock + private TargetInformationAdapter targetInformationAdapter; + + @Mock + private static TargetInformation targetInformation; + + private RbacRuleAdapter rbacRuleAdapter; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + rbacRuleAdapter = new RbacRuleAdapter(targetInformationAdapter); + } + + @Test + public void adaptRbacRuleToSubmodelElementCollection() { + + String role = "admin"; + List actions = Arrays.asList(Action.READ); + + String rbacRuleKey = RbacRuleKeyGenerator.generateKey(role, actions.get(0).toString(), targetInformation.getClass().getName()); + + RbacRule rbacRule = new RbacRule(role, actions, targetInformation); + + SubmodelElementCollection expectedSMCRule = createDummySMC(rbacRule, rbacRuleKey); + + SubmodelElementCollection expectedTargetInformation = createDummyTargetInformation(); + + when(targetInformationAdapter.adapt(targetInformation)).thenReturn(expectedTargetInformation); + + SubmodelElementCollection actualSMCRule = rbacRuleAdapter.adapt(rbacRule, rbacRuleKey); + + assertEquals(rbacRuleKey, actualSMCRule.getIdShort()); + + List elements = actualSMCRule.getValue(); + assertEquals(3, elements.size()); + + Property roleProperty = (Property) elements.stream().filter(sme -> sme.getIdShort().equals("role")).findAny().get(); + assertEquals(role, roleProperty.getValue()); + + SubmodelElementList actionList = (SubmodelElementList) elements.stream().filter(sme -> sme.getIdShort().equals("action")).findAny().get(); + List actionValues = actionList.getValue().stream().map(Property.class::cast).map(Property::getValue).map(String::valueOf).collect(Collectors.toList()); + assertEquals(Arrays.asList("READ"), actionValues); + + SubmodelElementCollection actualTargetInformation = (SubmodelElementCollection) elements.stream().filter(sme -> sme.getIdShort().equals("targetInformation")).findAny().get(); + assertSame(expectedTargetInformation, actualTargetInformation); + + assertEquals(expectedSMCRule, actualSMCRule); + } + + @Test + public void adaptSubmodelElementCollectionToRbacRule() { + + RbacRule expectedRbacRule = createDummyRbacRule("admin", Arrays.asList(Action.READ), targetInformation); + + String rbacRuleKey = RbacRuleKeyGenerator.generateKey(expectedRbacRule.getRole(), expectedRbacRule.getAction().get(0).toString(), expectedRbacRule.getTargetInformation().getClass().getName()); + + when(targetInformationAdapter.adapt(createDummyTargetInformation())).thenReturn(targetInformation); + + SubmodelElementCollection rbacRuleSMC = createDummySMC(expectedRbacRule, rbacRuleKey); + + RbacRule actualRbacRule = rbacRuleAdapter.adapt(rbacRuleSMC); + + assertEquals(expectedRbacRule, actualRbacRule); + } + + @Test + public void targetInformationAdapterInvoked() { + String rbacRuleKey = "testRuleKey"; + String role = "admin"; + List actions = Arrays.asList(Action.READ); + + RbacRule rbacRule = new RbacRule(role, actions, targetInformation); + + SubmodelElementCollection expectedTargetInformation = createDummyTargetInformation(); + + when(targetInformationAdapter.adapt(targetInformation)).thenReturn(expectedTargetInformation); + + rbacRuleAdapter.adapt(rbacRule, rbacRuleKey); + + verify(targetInformationAdapter, times(1)).adapt(targetInformation); + } + + + public static SubmodelElementCollection createDummySMC(RbacRule rbacRule, String rbacRuleKey) { + + SubmodelElementCollection rule = new DefaultSubmodelElementCollection.Builder().idShort(rbacRuleKey).build(); + + Property role = new DefaultProperty.Builder().idShort("role").value(rbacRule.getRole()).build(); + SubmodelElementList action = new DefaultSubmodelElementList.Builder().idShort("action").build(); + + List actions = rbacRule.getAction().stream().map(RbacRuleAdapterTest::transform).collect(Collectors.toList()); + action.setValue(actions); + + SubmodelElementCollection targetInformation = createDummyTargetInformation(); + + rule.setValue(Arrays.asList(role, action, targetInformation)); + + return rule; + } + + public static RbacRule createDummyRbacRule(String role, List actions, TargetInformation targetInformation) { + + return new RbacRule(role, actions, targetInformation); + } + + private static SubmodelElementCollection createDummyTargetInformation() { + SubmodelElementCollection targetInformation = new DefaultSubmodelElementCollection.Builder().idShort("targetInformation").build(); + + SubmodelElementList targetInformationSML = new DefaultSubmodelElementList.Builder().idShort("aasIds").build(); + + Property targetInformationProperty = new DefaultProperty.Builder().value("dummyAasId").build(); + + targetInformationSML.setValue(Arrays.asList(targetInformationProperty)); + + targetInformation.setValue(Arrays.asList(targetInformationSML)); + + return targetInformation; + } + + private static Property transform(Action action) { + return new DefaultProperty.Builder().value(action.toString()).build(); + } + +} diff --git a/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelRbacStorageTest.java b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelRbacStorageTest.java new file mode 100644 index 000000000..3397c1ed3 --- /dev/null +++ b/basyx.common/basyx.authorization.rules.rbac.backend.submodel/src/test/java/org/eclipse/digitaltwin/basyx/authorization/rules/rbac/backend/submodel/SubmodelRbacStorageTest.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.authorization.rules.rbac.backend.submodel; + +import org.mockito.Mock; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.basyx.authorization.RbacStorageTestSuite; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleKeyGenerator; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelservice.client.ConnectedSubmodelService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +/** + * Tests {@link SubmodelAuthorizationRbacStorage} + * + * @author danish + */ +public class SubmodelRbacStorageTest extends RbacStorageTestSuite { + + private static final String ROLE_DUMMY_DEVELOPER = "Dummy_Developer"; + + private static final String ROLE_DUMMY_ENGINEER = "Dummy_Engineer"; + + private static final List DELETE_ACTIONS = Arrays.asList(Action.CREATE); + + private static final String DELETE_ROLE = "Auditer"; + + @Mock + private ConnectedSubmodelService smService; + + @Mock + private RbacRuleAdapter ruleAdapter; + + @Before + @Override + public void setUp() { + MockitoAnnotations.openMocks(this); + + setUpRbacStorage(); + } + + @Override + protected void setUpRbacStorage() { + rbacStorage = new SubmodelAuthorizationRbacStorage(smService, new HashMap<>(), ruleAdapter); + + RbacRule rule = RbacRuleAdapterTest.createDummyRbacRule("Admin", DELETE_ACTIONS, mock(TargetInformation.class)); + + SubmodelElementCollection smc = RbacRuleAdapterTest.createDummySMC(rule, createDummyKey(rule.getRole(), rule.getAction(), rule.getTargetInformation().getClass().getName())); + + createMockExpectations(smc); + } + + @Test + @Override + public void testRemoveRule() { + + RbacRule expectedRule = createRbacRule(DELETE_ROLE, DELETE_ACTIONS, targetInformation); + + String key = createDummyKey(DELETE_ROLE, DELETE_ACTIONS, targetInformation.getClass().getName()); + + rbacStorage.addRule(expectedRule); + + rbacStorage.removeRule(key); + + assertFalse(rbacStorage.exist(key)); + } + + private String createDummyKey(String role, List actions, String clazz) { + return RbacRuleKeyGenerator.generateKey(role, actions.get(0).toString(), clazz); + } + + private void createMockExpectations(SubmodelElementCollection smc) { + when(ruleAdapter.adapt(any(RbacRule.class), anyString())).thenReturn(smc); + + when(smService.getSubmodelElement(anyString())).thenAnswer(invocation -> { + String key = invocation.getArgument(0, String.class); + if ("nonexistentKey".equals(key)) { + throw new ElementDoesNotExistException(); + } + + return smc; + }); + + when(smService.getSubmodelElement(createDummyKey(DELETE_ROLE, DELETE_ACTIONS, targetInformation.getClass().getName()))).thenThrow(new ElementDoesNotExistException()); + + when(ruleAdapter.adapt(any(SubmodelElementCollection.class))).thenAnswer(invocation -> { + SubmodelElementCollection smcRule = invocation.getArgument(0, SubmodelElementCollection.class); + + String role = smcRule.getValue().stream().filter(sme -> sme.getIdShort().equals("role")).map(Property.class::cast).map(Property::getValue).findAny().get(); + + if (role.equals(ROLE_DUMMY_ENGINEER) || role.equals(ROLE_DUMMY_DEVELOPER)) + return RbacRuleAdapterTest.createDummyRbacRule(role, DELETE_ACTIONS, mock(TargetInformation.class)); + + return expectedRule; + }); + + @SuppressWarnings("unchecked") + CursorResult> cursorResult = mock(CursorResult.class); + + when(smService.getSubmodelElements(any(PaginationInfo.class))).thenReturn(cursorResult); + + when(cursorResult.getResult()).thenAnswer(new Answer>() { + + @Override + public List answer(InvocationOnMock invocation) throws Throwable { + RbacRule rbacRule1 = RbacRuleAdapterTest.createDummyRbacRule(ROLE_DUMMY_ENGINEER, Arrays.asList(Action.READ), mock(TargetInformation.class)); + RbacRule rbacRule2 = RbacRuleAdapterTest.createDummyRbacRule(ROLE_DUMMY_DEVELOPER, Arrays.asList(Action.READ), mock(TargetInformation.class)); + + SubmodelElement rule1 = RbacRuleAdapterTest.createDummySMC(rbacRule1, createDummyKey(rbacRule1.getRole(), rbacRule1.getAction(), rbacRule1.getTargetInformation().getClass().getName())); + SubmodelElement rule2 = RbacRuleAdapterTest.createDummySMC(rbacRule2, createDummyKey(rbacRule2.getRole(), rbacRule2.getAction(), rbacRule2.getTargetInformation().getClass().getName())); + return Arrays.asList(rule1, rule2); + } + + }); + } + +} diff --git a/basyx.common/basyx.authorization/pom.xml b/basyx.common/basyx.authorization/pom.xml index 938052680..5a6342b95 100644 --- a/basyx.common/basyx.authorization/pom.xml +++ b/basyx.common/basyx.authorization/pom.xml @@ -11,7 +11,7 @@ basyx.authorization - + BaSyx Authorization Base BaSyx Authorization Base @@ -62,5 +62,11 @@ org.reflections reflections + + org.mockito + mockito-core + test + + \ No newline at end of file diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/CommonAuthorizationProperties.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/CommonAuthorizationProperties.java index 55c7e9776..5c790737d 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/CommonAuthorizationProperties.java +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/CommonAuthorizationProperties.java @@ -41,4 +41,14 @@ private CommonAuthorizationProperties() { public static final String TYPE_PROPERTY_KEY = PROPERTIES_PREFIX + ".type"; public static final String RBAC_FILE_PROPERTY_KEY = PROPERTIES_PREFIX + ".rbac.file"; public static final String JWT_BEARER_TOKEN_PROVIDER_PROPERTY_KEY = PROPERTIES_PREFIX + ".jwtBearerTokenProvider"; + public static final String RULES_BACKEND_TYPE = PROPERTIES_PREFIX + ".rules.backend"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION = RULES_BACKEND_TYPE + ".submodel.authorization"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_SUBMODEL_ENDPOINT = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".endpoint"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_TOKEN_ENDPOINT = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".token-endpoint"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_GRANT_TYPE = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".grant-type"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_CLIENT_ID = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".client-id"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_CLIENT_SECRET = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".client-secret"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_USERNAME = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".username"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_PASSWORD = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".password"; + public static final String RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION_SCOPES = RULES_BACKEND_TYPE_SUBMODEL_AUTHORIZATION + ".scopes"; } diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/Action.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/Action.java index 7bad289e4..9efd9bcff 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/Action.java +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/Action.java @@ -36,5 +36,14 @@ public enum Action { CREATE, UPDATE, EXECUTE, - DELETE + DELETE; + + public static Action fromString(String action) { + try { + return Action.valueOf(action.toUpperCase()); + } catch (IllegalArgumentException e) { + // Handle the case where the string does not match any enum constant + return null; // Or throw an exception, or use a default value + } + } } diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleDeserializer.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleDeserializer.java index 5795a4705..677226e99 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleDeserializer.java +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleDeserializer.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -50,11 +51,16 @@ public HashMap deserialize(JsonParser p, DeserializationContex HashMap result = new HashMap<>(); for (RbacRule rule : rbacRules) { - rule.getAction().stream().map(action -> RbacRuleKeyGenerator.generateKey(rule.getRole(), action.toString(), rule.getTargetInformation().getClass().getName())).forEach(key -> result.put(key, rule)); + rule.getAction().stream().map(action -> new RbacRule(rule.getRole(), Arrays.asList(action), rule.getTargetInformation())).forEach(rbacRule -> result.put(createKey(rbacRule), rbacRule)); } return result; } + + private String createKey(RbacRule rbacRule) { + + return RbacRuleKeyGenerator.generateKey(rbacRule.getRole(), rbacRule.getAction().get(0).toString(), rbacRule.getTargetInformation().getClass().getName()); + } } diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleKeyGenerator.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleKeyGenerator.java index 7ecba11a5..2df28bd33 100644 --- a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleKeyGenerator.java +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/RbacRuleKeyGenerator.java @@ -25,6 +25,8 @@ package org.eclipse.digitaltwin.basyx.authorization.rbac; +import java.util.Base64; + /** * A helper class to generate the key based on hash of combination of role, {@link Action}, and the concrete {@link TargetInformation} * class. @@ -49,7 +51,9 @@ public class RbacRuleKeyGenerator { * @return */ public static String generateKey(String role, String action, String clazz) { - return String.valueOf((role + action + clazz).hashCode()); + String combinedString = String.valueOf(role + action + clazz); + + return Base64.getEncoder().encodeToString(combinedString.getBytes()); } } diff --git a/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/TargetInformationTypeProcessor.java b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/TargetInformationTypeProcessor.java new file mode 100644 index 000000000..63fb6d8f0 --- /dev/null +++ b/basyx.common/basyx.authorization/src/main/java/org/eclipse/digitaltwin/basyx/authorization/rbac/TargetInformationTypeProcessor.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.authorization.rbac; + +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; + +import java.util.Set; + +/** + * A processor to detect the type of the {@link TargetInformation} at runtime. + * + * @author danish + */ +public class TargetInformationTypeProcessor { + + public static Object getImplementation(String type) { + Reflections reflections = new Reflections("org.eclipse.digitaltwin.basyx", Scanners.TypesAnnotated); + + Set> annotatedClasses = reflections.getTypesAnnotatedWith(TargetInformationSubtype.class); + + for (Class clazz : annotatedClasses) { + TargetInformationSubtype annotation = clazz.getAnnotation(TargetInformationSubtype.class); + if (annotation != null && annotation.getValue().equals(type)) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + return null; + } +} diff --git a/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/RbacStorageTestSuite.java b/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/RbacStorageTestSuite.java new file mode 100644 index 000000000..8dba39e65 --- /dev/null +++ b/basyx.common/basyx.authorization/src/test/java/org/eclipse/digitaltwin/basyx/authorization/RbacStorageTestSuite.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.eclipse.digitaltwin.basyx.authorization.rbac.Action; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRule; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacRuleKeyGenerator; +import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage; +import org.eclipse.digitaltwin.basyx.authorization.rbac.TargetInformation; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Test suite for {@link RbacStorage} + * + * @author danish + */ +public abstract class RbacStorageTestSuite { + + @Mock + protected TargetInformation targetInformation; + + protected RbacStorage rbacStorage; + + protected RbacRule expectedRule; + + protected abstract void setUpRbacStorage(); + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + + setUpRbacStorage(); + } + + @Test + public void testAddRule() { + + expectedRule = createRbacRule("Engineer", Arrays.asList(Action.READ), targetInformation); + + String key = RbacRuleKeyGenerator.generateKey(expectedRule.getRole(), expectedRule.getAction().get(0).toString(), expectedRule.getTargetInformation().getClass().getName()); + + rbacStorage.addRule(expectedRule); + + assertTrue(rbacStorage.exist(key)); + } + + @Test + public void testGetRbacRule() { + + expectedRule = createRbacRule("Maintainer", Arrays.asList(Action.CREATE), targetInformation); + + String key = RbacRuleKeyGenerator.generateKey(expectedRule.getRole(), expectedRule.getAction().get(0).toString(), expectedRule.getTargetInformation().getClass().getName()); + + rbacStorage.addRule(expectedRule); + + RbacRule retrievedRule = rbacStorage.getRbacRule(key); + + assertNotNull(retrievedRule); + assertEquals(expectedRule, retrievedRule); + } + + @Test + public void testRemoveRule() { + + expectedRule = createRbacRule("Supplier", Arrays.asList(Action.EXECUTE), targetInformation); + + String key = RbacRuleKeyGenerator.generateKey(expectedRule.getRole(), expectedRule.getAction().get(0).toString(), expectedRule.getTargetInformation().getClass().getName()); + + rbacStorage.addRule(expectedRule); + + rbacStorage.removeRule(key); + + assertFalse(rbacStorage.exist(key)); + } + + @Test + public void testExistWhenElementExists() { + + expectedRule = createRbacRule("Developer", Arrays.asList(Action.UPDATE), targetInformation); + + String key = RbacRuleKeyGenerator.generateKey(expectedRule.getRole(), expectedRule.getAction().get(0).toString(), expectedRule.getTargetInformation().getClass().getName()); + + rbacStorage.addRule(expectedRule); + + boolean exists = rbacStorage.exist(key); + + assertTrue(exists); + } + + @Test + public void testExistWhenElementDoesNotExist() { + + boolean exists = rbacStorage.exist("nonexistentKey"); + + assertFalse(exists); + } + + @Test + public void testGetRbacRules() { + + RbacRule rule1 = createRbacRule("Role_1", Collections.singletonList(Action.CREATE), targetInformation); + RbacRule rule2 = createRbacRule("Role_2", Collections.singletonList(Action.READ), targetInformation); + + rbacStorage.addRule(rule1); + rbacStorage.addRule(rule2); + + Map result = rbacStorage.getRbacRules(); + + assertNotNull(result); + assertEquals(2, result.size()); + } + + public RbacRule createRbacRule(String role, List actions, TargetInformation targetInformation) { + return new RbacRule(role, actions, targetInformation); + } + +} diff --git a/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/InvalidTargetInformationException.java b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/InvalidTargetInformationException.java new file mode 100644 index 000000000..14caafc20 --- /dev/null +++ b/basyx.common/basyx.core/src/main/java/org/eclipse/digitaltwin/basyx/core/exceptions/InvalidTargetInformationException.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (C) 2024 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.core.exceptions; + +/** + * Indicates failure while handling the TargetInformation + * + * @author danish + * + */ +@SuppressWarnings("serial") +public class InvalidTargetInformationException extends RuntimeException { + + public InvalidTargetInformationException() { + super(); + } + + public InvalidTargetInformationException(String message) { + super(message); + } + +} diff --git a/basyx.common/pom.xml b/basyx.common/pom.xml index 16b9c06f6..9ac32b4b3 100644 --- a/basyx.common/pom.xml +++ b/basyx.common/pom.xml @@ -23,5 +23,7 @@ basyx.filerepository-backend basyx.filerepository-backend-inmemory basyx.filerepository-backend-mongodb + basyx.authorization.rules.rbac.backend.inmemory + basyx.authorization.rules.rbac.backend.submodel \ No newline at end of file diff --git a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-feature-authorization/pom.xml b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-feature-authorization/pom.xml index 568cbdf5c..c80d7491a 100644 --- a/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-feature-authorization/pom.xml +++ b/basyx.conceptdescriptionrepository/basyx.conceptdescriptionrepository-feature-authorization/pom.xml @@ -64,5 +64,9 @@ commons-io commons-io + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml index be21bfb3b..64ec4f3e5 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/pom.xml @@ -57,5 +57,9 @@ basyx.submodelregistry-service-inmemory-storage test + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/pom.xml b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/pom.xml index b62289e4a..dccab08e1 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/pom.xml +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/pom.xml @@ -60,6 +60,10 @@ basyx.submodelservice-backend-inmemory test + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + org.apache.httpcomponents.client5 httpclient5 diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/MultiUrlSubmodelRepositoryRegistryLinkTest.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/MultiUrlSubmodelRepositoryRegistryLinkTest.java index e34ae3d07..a21b4b0f2 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/MultiUrlSubmodelRepositoryRegistryLinkTest.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/MultiUrlSubmodelRepositoryRegistryLinkTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (C) 2023 the Eclipse BaSyx Authors + * 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 diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index ff216c98d..92e30a58b 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -136,6 +136,26 @@ services: networks: - basyx-java-server-sdk + configuration-sm-repo: + image: eclipsebasyx/submodel-repository:$BASYX_VERSION + container_name: configuration-sm-repo + ports: + - "8055:8081" + environment: + SERVER_SERVLET_CONTEXT_PATH: / + BASYX_CORS_ALLOWED_ORIGINS: '*' + BASYX_CORS_ALLOWED_METHODS: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD + BASYX_FEATURE_AUTHORIZATION_ENABLED: true + BASYX_FEATURE_AUTHORIZATION_TYPE: rbac + BASYX_FEATURE_AUTHORIZATION_JWTBEARERTOKENPROVIDER: keycloak + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/BaSyx + BASYX_FEATURE_AUTHORIZATION_RBAC_FILE: file:/rbac/rbac_rules.json + volumes: + - ./keycloak/rules/rbac_rules-conf-sm-repo.json:/rbac/rbac_rules.json:ro + restart: always + networks: + - basyx-java-server-sdk + keycloak: build: context: ./keycloak diff --git a/ci/keycloak/rules/rbac_rules-conf-sm-repo.json b/ci/keycloak/rules/rbac_rules-conf-sm-repo.json new file mode 100644 index 000000000..ef11e18f6 --- /dev/null +++ b/ci/keycloak/rules/rbac_rules-conf-sm-repo.json @@ -0,0 +1,155 @@ +[ + { + "role": "basyx-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "admin", + "action": ["CREATE", "READ", "UPDATE", "DELETE", "EXECUTE"], + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": ["specificSubmodelId", "testSMId1", "testSMId2"], + "submodelElementIdShortPaths": ["testSMEIdShortPath1","smc2.specificSubmodelElementIdShort","testSMEIdShortPath2"] + } + }, + { + "role": "basyx-sme-reader-two", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-creator", + "action": "CREATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-sme-updater-two", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "smc2" + } + }, + { + "role": "basyx-sme-updater-three", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc1.specificSubmodelElementIdShort-2" + } + }, + { + "role": "basyx-file-sme-updater", + "action": "UPDATE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + }, + { + "role": "basyx-deleter", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-deleter-two", + "action": "DELETE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "*", + "submodelElementIdShortPaths": "*" + } + }, + { + "role": "basyx-executor-two", + "action": "EXECUTE", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId", + "submodelElementIdShortPaths": "square" + } + }, + { + "role": "basyx-file-sme-reader", + "action": "READ", + "targetInformation": { + "@type": "submodel", + "submodelIds": "specificSubmodelId-2", + "submodelElementIdShortPaths": "smc2.specificFileSubmodelElementIdShort" + } + } +] \ No newline at end of file diff --git a/pom.xml b/pom.xml index f47392382..eae345ed8 100644 --- a/pom.xml +++ b/pom.xml @@ -449,6 +449,16 @@ basyx.authorization ${revision} + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.submodel + ${revision} + org.eclipse.digitaltwin.basyx basyx.filerepository-backend @@ -817,6 +827,18 @@ ${revision} tests + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.inmemory + ${revision} + tests + + + org.eclipse.digitaltwin.basyx + basyx.authorization.rules.rbac.backend.submodel + ${revision} + tests + org.eclipse.digitaltwin.basyx