From d28d43bdc0c18450dba389ef0b043cb974ff6fa9 Mon Sep 17 00:00:00 2001 From: Istvan Zoltan Nagy Date: Wed, 14 Feb 2024 15:09:47 +0100 Subject: [PATCH] Implementation of CRUD API for Access management APIs - Fixes OpenAPI schema as generated classes were not representing policy details accurately - Implements API delegate and Service classes responsible for CRUD operations - Removes file based repository implementation - Changes repository method to accept Instant for validity checks - Defines the new table in Liquibase configuration - Updates existing test cases - Adds new OAuth2 roles to provide read/write access to access rules - Mentions the new roles in the readme --- access-control-service-sql-impl/pom.xml | 4 + .../controller/AccessControlApiDelegate.java | 55 ++++++- .../accesscontrol/sql/model/AccessRule.java | 6 +- .../converter/CustomAccessRuleMapper.java | 14 +- .../AccessControlRuleRepository.java | 17 ++- .../FileBasedAccessControlRuleRepository.java | 75 --------- .../AccessControlPersistenceService.java | 40 +++++ .../AccessControlPersistenceServiceImpl.java | 118 ++++++++++++++ .../SqlBackedAccessControlRuleService.java | 21 ++- .../access-control-openapi_schemas.yaml | 10 +- .../model/converter/AccessRuleMapperTest.java | 32 ++-- ...eBasedAccessControlRuleRepositoryTest.java | 72 --------- ...SqlBackedAccessControlRuleServiceTest.java | 31 +++- backend/pom.xml | 4 + .../semantics/ApiExceptionHandler.java | 16 +- .../security/AuthorizationEvaluator.java | 10 ++ .../security/OAuthSecurityConfig.java | 9 ++ .../db/changelog/db.changelog-master.yaml | 11 ++ .../db/changelog/db.changelog-v4.yaml | 88 +++++++++++ ...setAdministrationShellApiSecurityTest.java | 95 +++++++----- ...setAdministrationShellApiSecurityTest.java | 144 ++++++++++++++---- .../tractusx/semantics/registry/TestUtil.java | 35 +++++ .../test/resources/application-granular.yml | 4 +- docs/README.md | 2 + pom.xml | 11 +- 25 files changed, 658 insertions(+), 266 deletions(-) delete mode 100644 access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepository.java create mode 100644 access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceService.java create mode 100644 access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceServiceImpl.java delete mode 100644 access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepositoryTest.java create mode 100644 backend/src/main/resources/db/changelog/db.changelog-v4.yaml diff --git a/access-control-service-sql-impl/pom.xml b/access-control-service-sql-impl/pom.xml index aa98f5a6..612d4b08 100644 --- a/access-control-service-sql-impl/pom.xml +++ b/access-control-service-sql-impl/pom.xml @@ -74,6 +74,10 @@ org.springframework spring-tx + + org.springframework.data + spring-data-jpa + jakarta.servlet jakarta.servlet-api diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/controller/AccessControlApiDelegate.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/controller/AccessControlApiDelegate.java index 462f4dcd..9a615a7d 100644 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/controller/AccessControlApiDelegate.java +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/controller/AccessControlApiDelegate.java @@ -20,10 +20,63 @@ package org.eclipse.tractusx.semantics.accesscontrol.sql.controller; +import java.util.List; + +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.converter.AccessRuleMapper; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.api.AccessControlsApiDelegate; +import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.CreateAccessRule; +import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.GetAllAccessRules200Response; +import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.ReadUpdateAccessRule; +import org.eclipse.tractusx.semantics.accesscontrol.sql.service.AccessControlPersistenceService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -//TODO: must be implemented @Service public class AccessControlApiDelegate implements AccessControlsApiDelegate { + + private final AccessControlPersistenceService accessControlPersistenceService; + private final AccessRuleMapper accessRuleMapper; + + public AccessControlApiDelegate( final AccessControlPersistenceService accessControlPersistenceService, AccessRuleMapper accessRuleMapper ) { + this.accessControlPersistenceService = accessControlPersistenceService; + this.accessRuleMapper = accessRuleMapper; + } + + @Override + public ResponseEntity createNewAccessRule( CreateAccessRule createAccessRule ) { + AccessRule savedRule = accessControlPersistenceService.saveRule( accessRuleMapper.map( createAccessRule ) ); + return ResponseEntity.status( HttpStatus.CREATED ).body( accessRuleMapper.map( savedRule ) ); + } + + @Override + public ResponseEntity deleteAccessRuleByRuleId( Long ruleId ) { + accessControlPersistenceService.deleteRule( ruleId ); + return ResponseEntity.status( HttpStatus.NO_CONTENT ).build(); + } + + @Override + public ResponseEntity getAccessRuleByRuleId( Long ruleId ) { + return accessControlPersistenceService.getRuleById( ruleId ) + .map( accessRuleMapper::map ) + .map( rule -> ResponseEntity.ok().body( rule ) ) + .orElse( ResponseEntity.notFound().build() ); + } + + @Override + public ResponseEntity getAllAccessRules() { + List items = accessControlPersistenceService.getAllRules().stream() + .map( accessRuleMapper::map ) + .toList(); + GetAllAccessRules200Response response = new GetAllAccessRules200Response(); + response.setItems( items ); + return ResponseEntity.ok().body( response ); + } + + @Override + public ResponseEntity updateAccessRuleByRuleId( Long ruleId, ReadUpdateAccessRule readUpdateAccessRule ) { + AccessRule updatedRule = accessControlPersistenceService.updateRule( ruleId, accessRuleMapper.map( readUpdateAccessRule ) ); + return ResponseEntity.ok().body( accessRuleMapper.map( updatedRule ) ); + } } diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/AccessRule.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/AccessRule.java index 4ee62c2d..6a1fba54 100644 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/AccessRule.java +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/AccessRule.java @@ -29,20 +29,22 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; +import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Lob; +import jakarta.persistence.Table; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; import lombok.Data; -//@Entity -//@Table( name = "ACCESS_RULE" ) +@Entity +@Table( name = "ACCESS_RULE" ) @Data @ValidValidityPeriod( groups = { OnCreate.class, OnUpdate.class } ) public class AccessRule { diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/CustomAccessRuleMapper.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/CustomAccessRuleMapper.java index a1145405..7d86bdef 100644 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/CustomAccessRuleMapper.java +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/CustomAccessRuleMapper.java @@ -20,16 +20,28 @@ package org.eclipse.tractusx.semantics.accesscontrol.sql.model.converter; +import org.apache.commons.lang3.StringUtils; import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; import org.mapstruct.AfterMapping; import org.mapstruct.MappingTarget; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class CustomAccessRuleMapper { + private final String ownerTenant; + + public CustomAccessRuleMapper( @Value( "${registry.idm.owning-tenant-id:}" ) String ownerTenant ) { + this.ownerTenant = StringUtils.stripToNull( ownerTenant ); + } + @AfterMapping - public void calledWithSourceAndTarget(Object anySource, @MappingTarget AccessRule target) { + public void calledWithTarget( @MappingTarget AccessRule target ) { target.setTargetTenant( target.getPolicy().getBpn() ); + // only fill Tid if null to avoid overwrites in case of updates + if ( target.getTid() == null ) { + target.setTid( ownerTenant ); + } } } diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/AccessControlRuleRepository.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/AccessControlRuleRepository.java index a5e8be33..c2db565c 100644 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/AccessControlRuleRepository.java +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/AccessControlRuleRepository.java @@ -20,11 +20,24 @@ package org.eclipse.tractusx.semantics.accesscontrol.sql.repository; +import java.time.Instant; import java.util.List; import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; -public interface AccessControlRuleRepository { +@Repository +public interface AccessControlRuleRepository extends JpaRepository { - List findAllByBpnWithinValidityPeriod( String bpn, String bpnWildcard ); + @Query( """ + SELECT r + FROM AccessRule r + WHERE + r.targetTenant IN (:bpn, :bpnWildcard) + AND ( r.validFrom IS NULL OR r.validFrom <= :now ) + AND ( r.validTo IS NULL OR r.validTo >= :now ) + """ ) + List findAllByBpnWithinValidityPeriod( String bpn, String bpnWildcard, Instant now ); } diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepository.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepository.java deleted file mode 100644 index 425e01ae..00000000 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepository.java +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others - * Copyright (c) 2024 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - ******************************************************************************/ - -package org.eclipse.tractusx.semantics.accesscontrol.sql.repository; - -import java.io.IOException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.stereotype.Repository; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -@Repository -public class FileBasedAccessControlRuleRepository implements AccessControlRuleRepository { - - private static final TypeReference> RULE_LIST_TYPE = new TypeReference<>() { - }; - private final Path accessControlRulePath; - private final ObjectMapper objectMapper; - - public FileBasedAccessControlRuleRepository( - @Autowired ObjectMapper objectMapper, - @Value( "${ACCESS_CONTROL_RULES_PATH:access-control-rules.json}" ) String accessControlRulePath ) { - this.accessControlRulePath = Path.of( accessControlRulePath ); - this.objectMapper = objectMapper; - } - - @Override - public List findAllByBpnWithinValidityPeriod( final String bpn, final String bpnWildcard ) { - try { - Set bpns = Set.of( bpn, bpnWildcard ); - return objectMapper.readValue( accessControlRulePath.toFile(), RULE_LIST_TYPE ).stream() - .filter( rule -> bpns.contains( rule.getTargetTenant() ) ) - .filter( rule -> { - Instant now = Instant.now(); - final var validFromIsEmptyOrInThePast = Optional.ofNullable( rule.getValidFrom() ) - .map( now::isAfter ) - .orElse( true ); - final var validToIsEmptyOrInTheFuture = Optional.ofNullable( rule.getValidTo() ) - .map( now::isBefore ) - .orElse( true ); - return validFromIsEmptyOrInThePast && validToIsEmptyOrInTheFuture; - } ) - .toList(); - } catch ( IOException e ) { - throw new DataRetrievalFailureException( e.getMessage(), e ); - } - } -} diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceService.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceService.java new file mode 100644 index 00000000..20422077 --- /dev/null +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceService.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.accesscontrol.sql.service; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; + +public interface AccessControlPersistenceService { + + List getAllRules(); + + Optional getRuleById( Long ruleId ); + + AccessRule saveRule( AccessRule rule ); + + AccessRule updateRule( Long ruleId, AccessRule rule ); + + void deleteRule( Long ruleId ); + +} diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceServiceImpl.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceServiceImpl.java new file mode 100644 index 00000000..a216d224 --- /dev/null +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/AccessControlPersistenceServiceImpl.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.accesscontrol.sql.service; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; +import org.eclipse.tractusx.semantics.accesscontrol.sql.repository.AccessControlRuleRepository; +import org.eclipse.tractusx.semantics.accesscontrol.sql.validation.OnCreate; +import org.eclipse.tractusx.semantics.accesscontrol.sql.validation.OnUpdate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +@Service +public class AccessControlPersistenceServiceImpl implements AccessControlPersistenceService { + + private final AccessControlRuleRepository accessControlRuleRepository; + private final Validator validator; + private final String ownerTenant; + + public AccessControlPersistenceServiceImpl( + AccessControlRuleRepository accessControlRuleRepository, Validator validator, + @Value( "${registry.idm.owning-tenant-id:}" ) String ownerTenant ) { + this.accessControlRuleRepository = accessControlRuleRepository; + this.validator = validator; + this.ownerTenant = Objects.requireNonNull( StringUtils.stripToNull( ownerTenant ), "OwnerTenantId is not set!" ); + } + + @Override + @Transactional( propagation = Propagation.REQUIRED, readOnly = true ) + public List getAllRules() { + return accessControlRuleRepository.findAll(); + } + + @Override + @Transactional( propagation = Propagation.REQUIRED, readOnly = true ) + public Optional getRuleById( Long ruleId ) { + return accessControlRuleRepository.findById( ruleId ); + } + + @Override + @Transactional( propagation = Propagation.REQUIRED ) + public AccessRule saveRule( AccessRule rule ) { + verifyOwnerTenantId( rule ); + Set> violations = validator.validate( rule, OnCreate.class ); + if ( !violations.isEmpty() ) { + throw new ConstraintViolationException( violations ); + } + return accessControlRuleRepository.saveAndFlush( rule ); + } + + @Override + @Transactional( propagation = Propagation.REQUIRED ) + public AccessRule updateRule( Long ruleId, AccessRule rule ) { + verifyOwnerTenantId( rule ); + verifyRuleId( ruleId, rule ); + Set> violations = validator.validate( rule, OnUpdate.class ); + if ( !violations.isEmpty() ) { + throw new ConstraintViolationException( violations ); + } + final AccessRule entity = accessControlRuleRepository.findById( ruleId ) + .orElseThrow( () -> new IllegalStateException( "Rule with Id: " + ruleId + " cannot be updated as it does not exist!" ) ); + entity.setTid( rule.getTid() ); + entity.setTargetTenant( rule.getTargetTenant() ); + entity.setPolicy( rule.getPolicy() ); + entity.setPolicyType( rule.getPolicyType() ); + entity.setDescription( rule.getDescription() ); + entity.setValidFrom( rule.getValidFrom() ); + entity.setValidTo( rule.getValidTo() ); + return accessControlRuleRepository.saveAndFlush( entity ); + } + + @Override + @Transactional( propagation = Propagation.REQUIRED ) + public void deleteRule( Long ruleId ) { + accessControlRuleRepository.deleteById( ruleId ); + } + + private void verifyRuleId( Long ruleId, AccessRule rule ) { + if ( !Objects.equals( rule.getId(), ruleId ) ) { + throw new IllegalArgumentException( "RuleId must match the rule.id value!" ); + } + } + + private void verifyOwnerTenantId( AccessRule rule ) { + if ( !Objects.equals( rule.getTid(), ownerTenant ) ) { + throw new IllegalArgumentException( "TenantId must match the Id of the owner tenant: " + ownerTenant ); + } + } +} diff --git a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleService.java b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleService.java index 2095211f..51c857f2 100644 --- a/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleService.java +++ b/access-control-service-sql-impl/src/main/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleService.java @@ -20,6 +20,7 @@ package org.eclipse.tractusx.semantics.accesscontrol.sql.service; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; @@ -37,11 +38,15 @@ import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRulePolicy; import org.eclipse.tractusx.semantics.accesscontrol.sql.repository.AccessControlRuleRepository; +import org.springframework.dao.DataAccessException; import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class SqlBackedAccessControlRuleService implements AccessControlRuleService { + private static final String NO_MATCHING_RULES_ARE_FOUND = "No matching rules are found."; private final AccessControlRuleRepository repository; private final String bpnWildcard; @@ -97,11 +102,16 @@ public Map fetchVisibilityCriteriaForShells( Li } private Stream findPotentiallyMatchingAccessControlRules( String bpn ) throws DenyAccessException { - List allByBpn = repository.findAllByBpnWithinValidityPeriod( bpn, bpnWildcard ); - if ( allByBpn == null || allByBpn.isEmpty() ) { - throw new DenyAccessException( "No matching rules are found." ); + try { + List allByBpn = repository.findAllByBpnWithinValidityPeriod( bpn, bpnWildcard, Instant.now() ); + if ( allByBpn == null || allByBpn.isEmpty() ) { + throw new DenyAccessException( NO_MATCHING_RULES_ARE_FOUND ); + } + return allByBpn.stream().map( AccessRule::getPolicy ).filter( policy -> !policy.getMandatorySpecificAssetIds().isEmpty() ); + } catch ( DataAccessException e ) { + log.error( "Failed to fetch rules for BPN: " + bpn, e.getMessage() ); + throw new DenyAccessException( NO_MATCHING_RULES_ARE_FOUND ); } - return allByBpn.stream().map( AccessRule::getPolicy ); } private Set findMatchingAccessControlRules( ShellVisibilityContext shellContext, String bpn ) throws DenyAccessException { @@ -109,9 +119,8 @@ private Set findMatchingAccessControlRules( ShellVisibilityCon .filter( accessControlRule -> shellContext.specificAssetIds().containsAll( accessControlRule.getMandatorySpecificAssetIds() ) ) .collect( Collectors.toSet() ); if ( matching.isEmpty() ) { - throw new DenyAccessException( "No matching rules are found." ); + throw new DenyAccessException( NO_MATCHING_RULES_ARE_FOUND ); } return matching; } - } diff --git a/access-control-service-sql-impl/src/main/resources/static/access-control-openapi_schemas.yaml b/access-control-service-sql-impl/src/main/resources/static/access-control-openapi_schemas.yaml index a476b692..f9026591 100644 --- a/access-control-service-sql-impl/src/main/resources/static/access-control-openapi_schemas.yaml +++ b/access-control-service-sql-impl/src/main/resources/static/access-control-openapi_schemas.yaml @@ -29,9 +29,8 @@ schemas: description: The Id of the provider tenant minLength: 1 maxLength: 36 - pattern: "^[0-9]{8}-[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{12}$" type: string - example: "00000000-1111-2222-3333-444444444444" + example: "BPNL00000000000B" PolicyType: description: Type of the policy. Only AAS supported. type: string @@ -80,6 +79,8 @@ schemas: type: string operator: $ref: "#/schemas/OperatorType" + value: + type: string values: type: array items: @@ -89,7 +90,6 @@ schemas: required: - attribute - operator - - values additionalProperties: false AasPolicy: description: Describes an AAS policy @@ -99,9 +99,7 @@ schemas: type: array description: The components of the Access Rule Policy items: - "anyOf": - - { $ref: "#/schemas/AccessRuleValue" } - - { $ref: "#/schemas/AccessRuleValues" } + $ref: "#/schemas/AccessRuleValues" minLength: 1 uniqueItems: true required: diff --git a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/AccessRuleMapperTest.java b/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/AccessRuleMapperTest.java index 76d3bb80..9c3d7569 100644 --- a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/AccessRuleMapperTest.java +++ b/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/model/converter/AccessRuleMapperTest.java @@ -36,8 +36,8 @@ import org.eclipse.tractusx.semantics.accesscontrol.sql.model.policy.AccessRulePolicyValue; import org.eclipse.tractusx.semantics.accesscontrol.sql.model.policy.PolicyOperator; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.AasPolicy; -import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.AasPolicyAccessRulesInner; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.AccessRuleValue; +import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.AccessRuleValues; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.CreateAccessRule; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.OperatorType; import org.eclipse.tractusx.semantics.accesscontrol.sql.rest.model.PolicyType; @@ -58,7 +58,7 @@ class AccessRuleMapperTest { private static final OffsetDateTime NOW_DATE = NOW.atOffset( ZoneOffset.UTC ); private static final OffsetDateTime ONE_MINUTE_AGO_DATE = ONE_MINUTE_AGO.atOffset( ZoneOffset.UTC ); - private final AccessRuleMapper underTest = new AccessRuleMapperImpl( new CustomAccessRuleMapper() ); + private final AccessRuleMapper underTest = new AccessRuleMapperImpl( new CustomAccessRuleMapper(INPUT_BPNA) ); @Test void testMapCreateAccessRuleWithFullyPopulatedDataExpectSuccess() { @@ -74,7 +74,7 @@ void testMapCreateAccessRuleWithFullyPopulatedDataExpectSuccess() { assertThat( actual ) .isNotNull() .hasFieldOrPropertyWithValue( "id", null ) - .hasFieldOrPropertyWithValue( "tid", null ) + .hasFieldOrPropertyWithValue( "tid", INPUT_BPNA ) .hasFieldOrPropertyWithValue( "targetTenant", INPUT_BPNA ) .hasFieldOrPropertyWithValue( "policyType", AccessRule.PolicyType.AAS ) .hasFieldOrPropertyWithValue( "description", INPUT_DESCRIPTION ) @@ -101,7 +101,7 @@ void testMapCreateAccessRuleWithMinimallyPopulatedDataExpectSuccess() { assertThat( actual ) .isNotNull() .hasFieldOrPropertyWithValue( "id", null ) - .hasFieldOrPropertyWithValue( "tid", null ) + .hasFieldOrPropertyWithValue( "tid", INPUT_BPNA ) .hasFieldOrPropertyWithValue( "targetTenant", INPUT_BPNA ) .hasFieldOrPropertyWithValue( "policyType", AccessRule.PolicyType.AAS ) .hasFieldOrPropertyWithValue( "description", null ) @@ -202,16 +202,16 @@ void testMapReadAccessRuleWithFullyPopulatedDataExpectSuccess() { assertThat( actual.getPolicy().getAccessRules() ) .isNotNull() .hasSize( 4 ) - .contains( new AasPolicyAccessRulesInner().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( INPUT_BPNB ).values( null ) ) - .contains( new AasPolicyAccessRulesInner().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( + .contains( new AccessRuleValues().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( INPUT_BPNB ) ) + .contains( new AccessRuleValues().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( INPUT_MANDATORY_SPEC_ASSET_IDS.entrySet().stream() .map( entry -> new AccessRuleValue().attribute( entry.getKey() ).operator( OperatorType.EQ ).value( entry.getValue() ) ) .collect( Collectors.toSet() ) ) ) - .contains( new AasPolicyAccessRulesInner().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ).operator( OperatorType.INCLUDES ).values( + .contains( new AccessRuleValues().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ).operator( OperatorType.INCLUDES ).values( INPUT_VISIBLE_SPEC_ASSET_ID_NAMES.stream() .map( item -> new AccessRuleValue().attribute( "name" ).operator( OperatorType.EQ ).value( item ) ) .collect( Collectors.toSet() ) ) ) - .contains( new AasPolicyAccessRulesInner().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( + .contains( new AccessRuleValues().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( INPUT_VISIBLE_SEMANTIC_IDS.stream() .map( item -> new AccessRuleValue().attribute( "modelUrn" ).operator( OperatorType.EQ ).value( item ) ) .collect( Collectors.toSet() ) ) ); @@ -238,30 +238,30 @@ void testMapReadAccessRuleWithMinimallyPopulatedDataExpectSuccess() { assertThat( actual.getPolicy().getAccessRules() ) .isNotNull() .hasSize( 4 ) - .contains( new AasPolicyAccessRulesInner().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( INPUT_BPNB ).values( null ) ) - .contains( new AasPolicyAccessRulesInner().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( + .contains( new AccessRuleValues().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( INPUT_BPNB ) ) + .contains( new AccessRuleValues().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).operator( OperatorType.INCLUDES ).values( INPUT_MANDATORY_SPEC_ASSET_IDS.entrySet().stream() .map( entry -> new AccessRuleValue().attribute( entry.getKey() ).operator( OperatorType.EQ ).value( entry.getValue() ) ) .collect( Collectors.toSet() ) ) ) - .contains(new AasPolicyAccessRulesInner().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ) + .contains(new AccessRuleValues().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ) .operator( OperatorType.INCLUDES ).values( Set.of() ) ) - .contains( new AasPolicyAccessRulesInner().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ) + .contains( new AccessRuleValues().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ) .operator( OperatorType.INCLUDES ).values( Set.of() ) ); } @SuppressWarnings( "SameParameterValue" ) private AasPolicy generatePolicy( String bpn, Map msaId, Set vsaId, Set semId ) { return new AasPolicy() - .addAccessRulesItem( new AasPolicyAccessRulesInner().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( bpn ) ) - .addAccessRulesItem( new AasPolicyAccessRulesInner().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).values( + .addAccessRulesItem( new AccessRuleValues().attribute( BPN_RULE_NAME ).operator( OperatorType.EQ ).value( bpn ) ) + .addAccessRulesItem( new AccessRuleValues().attribute( MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME ).values( msaId.entrySet().stream() .map( entity -> new AccessRuleValue().attribute( entity.getKey() ).operator( OperatorType.EQ ).value( entity.getValue() ) ) .collect( Collectors.toSet() ) ) ) - .addAccessRulesItem( new AasPolicyAccessRulesInner().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ).values( + .addAccessRulesItem( new AccessRuleValues().attribute( VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME ).values( vsaId.stream() .map( item -> new AccessRuleValue().attribute( "name" ).operator( OperatorType.EQ ).value( item ) ) .collect( Collectors.toSet() ) ) ) - .addAccessRulesItem( new AasPolicyAccessRulesInner().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ).values( + .addAccessRulesItem( new AccessRuleValues().attribute( VISIBLE_SEMANTIC_IDS_RULE_NAME ).values( semId.stream() .map( item -> new AccessRuleValue().attribute( "modelUrn" ).operator( OperatorType.EQ ).value( item ) ) .collect( Collectors.toSet() ) ) ); diff --git a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepositoryTest.java b/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepositoryTest.java deleted file mode 100644 index eb9bf1d7..00000000 --- a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/repository/FileBasedAccessControlRuleRepositoryTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others - * Copyright (c) 2024 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - ******************************************************************************/ - -package org.eclipse.tractusx.semantics.accesscontrol.sql.repository; - -import static org.assertj.core.api.Assertions.*; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; - -import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.dao.DataRetrievalFailureException; - -import com.fasterxml.jackson.databind.ObjectMapper; - -class FileBasedAccessControlRuleRepositoryTest { - - private static final String PUBLIC_READABLE = "PUBLIC_READABLE"; - private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); - - public static Stream bpnFilteringProvider() { - return Stream. builder() - .add( Arguments.of( "BPNL00000000000A", List.of( 1L, 3L ) ) ) - .add( Arguments.of( "BPNL00000000000B", List.of() ) ) - .add( Arguments.of( "BPNL00000000000C", List.of( 2L ) ) ) - .build(); - } - - @SuppressWarnings( "DataFlowIssue" ) - @ParameterizedTest - @MethodSource( "bpnFilteringProvider" ) - void testFindAllByBpnWithinValidityPeriodExpectFilteredResults( final String bpn, final List expectedRuleIds ) { - final var filePath = Path.of( getClass().getResource( "/example-access-rules.json" ).getFile() ); - final var underTest = new FileBasedAccessControlRuleRepository( objectMapper, filePath.toAbsolutePath().toString() ); - - List actual = underTest.findAllByBpnWithinValidityPeriod( bpn, PUBLIC_READABLE ); - - final var actualIds = actual.stream().map( AccessRule::getId ).toList(); - assertThat( actualIds ).isEqualTo( expectedRuleIds ); - } - - @Test - void testFindAllByBpnWithinValidityPeriodWithMissingResourceExpectException() { - final var filePath = Path.of( "unknown.json" ); - final var underTest = new FileBasedAccessControlRuleRepository( objectMapper, filePath.toAbsolutePath().toString() ); - - assertThatThrownBy( () -> underTest.findAllByBpnWithinValidityPeriod( "BPNL00000000000A", PUBLIC_READABLE ) ) - .isInstanceOf( DataRetrievalFailureException.class ); - } -} \ No newline at end of file diff --git a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleServiceTest.java b/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleServiceTest.java index fbefd32e..1c44c853 100644 --- a/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleServiceTest.java +++ b/access-control-service-sql-impl/src/test/java/org/eclipse/tractusx/semantics/accesscontrol/sql/service/SqlBackedAccessControlRuleServiceTest.java @@ -21,19 +21,25 @@ package org.eclipse.tractusx.semantics.accesscontrol.sql.service; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; -import java.nio.file.Path; +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.tractusx.semantics.accesscontrol.api.exception.DenyAccessException; import org.eclipse.tractusx.semantics.accesscontrol.api.model.ShellVisibilityContext; import org.eclipse.tractusx.semantics.accesscontrol.api.model.SpecificAssetId; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; import org.eclipse.tractusx.semantics.accesscontrol.sql.repository.AccessControlRuleRepository; -import org.eclipse.tractusx.semantics.accesscontrol.sql.repository.FileBasedAccessControlRuleRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -59,7 +65,6 @@ class SqlBackedAccessControlRuleServiceTest { private static final SpecificAssetId REVISION_NUMBER_02 = new SpecificAssetId( REVISION_NUMBER, "02" ); private static final String BPNA = "BPNL00000000000A"; private static final String BPNB = "BPNL00000000000B"; - private static final String BPNC = "BPNL00000000000C"; private static final String TRACEABILITYV_1_1_0 = "Traceability" + "v1.1.0"; private static final String PRODUCT_CARBON_FOOTPRINTV_1_1_0 = "ProductCarbonFootprintv1.1.0"; private SqlBackedAccessControlRuleService underTest; @@ -102,10 +107,24 @@ public static Stream matchingSpecificAssetIdVisibilityProvider() { } @BeforeEach - void setUp() { + void setUp() throws IOException { ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); - final var filePath = Path.of( getClass().getResource( "/example-access-rules.json" ).getFile() ); - AccessControlRuleRepository repository = new FileBasedAccessControlRuleRepository( objectMapper, filePath.toAbsolutePath().toString() ); + final var file = new File( Objects.requireNonNull( getClass().getResource( "/example-access-rules.json" ) ).getFile() ); + final List allRules = objectMapper.readerForListOf( AccessRule.class ).readValue( file ); + final var rulesByBpn = allRules.stream() + .filter( rule -> rule.getValidFrom() == null || rule.getValidFrom().isBefore( Instant.now() ) ) + .filter( rule -> rule.getValidTo() == null || rule.getValidTo().isAfter( Instant.now() ) ) + .collect( Collectors.groupingBy( AccessRule::getTargetTenant ) ); + AccessControlRuleRepository repository = mock(); + when( repository.findAll() ).thenReturn( allRules ); + when( repository.findAllByBpnWithinValidityPeriod( anyString(), anyString(), any( Instant.class ) ) ) + .thenAnswer( invocationOnMock -> { + String bpn = invocationOnMock.getArgument( 0, String.class ); + String wildcard = invocationOnMock.getArgument( 1, String.class ); + return Stream.of( bpn, wildcard ) + .flatMap( key -> rulesByBpn.getOrDefault( key, Collections.emptyList() ).stream() ) + .toList(); + } ); underTest = new SqlBackedAccessControlRuleService( repository, "PUBLIC_READABLE" ); } diff --git a/backend/pom.xml b/backend/pom.xml index 84527807..b03f05bf 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -73,6 +73,10 @@ + + org.hibernate.validator + hibernate-validator + jakarta.validation jakarta.validation-api diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/ApiExceptionHandler.java b/backend/src/main/java/org/eclipse/tractusx/semantics/ApiExceptionHandler.java index 3107e3ad..272d1fa5 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/ApiExceptionHandler.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/ApiExceptionHandler.java @@ -22,6 +22,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -43,6 +44,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; @ControllerAdvice public class ApiExceptionHandler extends ResponseEntityExceptionHandler { @@ -98,7 +100,19 @@ public ResponseEntity handleMethodArgumentNotSupportedException( final H @ResponseStatus( HttpStatus.BAD_REQUEST ) public ResponseEntity handleDuplicateKeyException( DuplicateKeyException duplicateKeyException ) { return new ResponseEntity<>( - new Result().messages( List.of( new Message().messageType( Message.MessageTypeEnum.ERROR ).text(duplicateKeyException.getMessage() ) ) ), + new Result().messages( List.of( new Message().messageType( Message.MessageTypeEnum.ERROR ).text( duplicateKeyException.getMessage() ) ) ), + HttpStatus.BAD_REQUEST ); + } + + @ExceptionHandler( { ConstraintViolationException.class } ) + @ResponseStatus( HttpStatus.BAD_REQUEST ) + public ResponseEntity handleConstraintViolationException( ConstraintViolationException constraintViolationException ) { + final var messages = constraintViolationException.getConstraintViolations().stream() + .map( violation -> new Message().messageType( Message.MessageTypeEnum.ERROR ) + .text( violation.getPropertyPath().toString() + ": " + violation.getMessage() ) ) + .toList(); + return new ResponseEntity<>( + new Result().messages( messages ), HttpStatus.BAD_REQUEST ); } diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java index 66043f19..e5036eb7 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java @@ -77,6 +77,14 @@ public boolean hasRoleSubmodelAccessControl() { return containsRole( ROLE_SUBMODEL_ACCESS_CONTROL ); } + public boolean hasRoleReadAccessRules() { + return containsRole( ROLE_READ_ACCESS_RULES ); + } + + public boolean hasRoleWriteAccessRules() { + return containsRole( ROLE_WRITE_ACCESS_RULES ); + } + private boolean containsRole( String role ) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if ( !(authentication instanceof JwtAuthenticationToken) ) { @@ -114,6 +122,8 @@ public static final class Roles { public static final String ROLE_ADD_DIGITAL_TWIN = "add_digital_twin"; public static final String ROLE_DELETE_DIGITAL_TWIN = "delete_digital_twin"; public static final String ROLE_SUBMODEL_ACCESS_CONTROL = "submodel_access_control"; + public static final String ROLE_READ_ACCESS_RULES = "read_access_rules"; + public static final String ROLE_WRITE_ACCESS_RULES = "write_access_rules"; } } diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java index 0e8e2cfa..9e5c1f4c 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java @@ -72,6 +72,15 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception { //submodel access control requires special role .requestMatchers( HttpMethod.POST, "/**/submodel-descriptor/authorized" ).access( "@authorizationEvaluator.hasRoleSubmodelAccessControl()" ) + + //read access rules + .requestMatchers( HttpMethod.GET, "/**/access-controls/rules" ).access( "@authorizationEvaluator.hasRoleReadAccessRules()" ) + .requestMatchers( HttpMethod.GET, "/**/access-controls/rules/**" ).access( "@authorizationEvaluator.hasRoleReadAccessRules()" ) + + //write access rules + .requestMatchers( HttpMethod.POST, "/**/access-controls/rules" ).access( "@authorizationEvaluator.hasRoleWriteAccessRules()" ) + .requestMatchers( HttpMethod.PUT, "/**/access-controls/rules/**" ).access( "@authorizationEvaluator.hasRoleWriteAccessRules()" ) + .requestMatchers( HttpMethod.DELETE, "/**/access-controls/rules/**" ).access( "@authorizationEvaluator.hasRoleWriteAccessRules()" ) ) .csrf(CsrfConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy( SessionCreationPolicy.STATELESS ) ) diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.yaml b/backend/src/main/resources/db/changelog/db.changelog-master.yaml index 87807d38..e2984f9a 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.yaml @@ -23,6 +23,14 @@ databaseChangeLog: name: uuid_type value: uuid dbms: postgresql, h2 + - property: + name: clob_type + value: CLOB + dbms: h2 + - property: + name: clob_type + value: TEXT + dbms: postgresql - property: name: uuid_function value: uuid_generate_v4() @@ -44,3 +52,6 @@ databaseChangeLog: - include: file: db.changelog-v3.yaml relativeToChangelogFile: true + - include: + file: db.changelog-v4.yaml + relativeToChangelogFile: true diff --git a/backend/src/main/resources/db/changelog/db.changelog-v4.yaml b/backend/src/main/resources/db/changelog/db.changelog-v4.yaml new file mode 100644 index 00000000..979e3404 --- /dev/null +++ b/backend/src/main/resources/db/changelog/db.changelog-v4.yaml @@ -0,0 +1,88 @@ +################################################################################ +# Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH and others +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +databaseChangeLog: + - changeSet: + id: 12022024-01 + author: istvan-nagy-epam + changes: + - createSequence: + cacheSize: 371717 + cycle: true + dataType: int + incrementBy: 50 + minValue: 1 + ordered: true + sequenceName: ACCESS_RULE_SEQ + startValue: 1 + - createTable: + tableName: ACCESS_RULE + columns: + - column: + name: ID + type: BIGINT + constraints: + primaryKey: true + validatePrimaryKey: true + nullable: false + validateNullable: true + autoIncrement: true + generationType: ALWAYS + startWith: 1 + incrementBy: 50 + - column: + name: TID + type: NVARCHAR(36) + constraints: + nullable: false + validateNullable: true + - column: + name: TARGET_TENANT + type: NVARCHAR(36) + constraints: + nullable: false + validateNullable: true + - column: + name: POLICY_TYPE + type: NVARCHAR(10) + constraints: + nullable: false + validateNullable: true + - column: + name: POLICY + type: ${clob_type} + constraints: + nullable: false + validateNullable: true + - column: + name: DESCRIPTION + type: NVARCHAR(256) + constraints: + nullable: true + - column: + name: VALID_FROM + type: timestamp + constraints: + nullable: true + - column: + name: VALID_TO + type: timestamp + constraints: + nullable: true \ No newline at end of file diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/AssetAdministrationShellApiSecurityTest.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/AssetAdministrationShellApiSecurityTest.java index 90e9176b..b99194ed 100644 --- a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/AssetAdministrationShellApiSecurityTest.java +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/AssetAdministrationShellApiSecurityTest.java @@ -616,10 +616,18 @@ void testRbacForFetchShellsByIds() throws Exception { @DisplayName( "Tenant based specificAssetId visibility test" ) class TenantBasedVisibilityTest { + String keyPrefix; + + @BeforeEach + void setUp() { + keyPrefix = UUID.randomUUID().toString(); + } + @Test public void testGetAllShellsWithDefaultClosedFilteredSpecificAssetIdsByTenantId() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); - shellPayload.setId( UUID.randomUUID().toString() ); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); List shellpayloadSpecificAssetIDs = shellPayload.getSpecificAssetIds(); shellpayloadSpecificAssetIDs.forEach( specificAssetId -> specificAssetId.setExternalSubjectId( null ) ); shellPayload.setSpecificAssetIds( shellpayloadSpecificAssetIDs ); @@ -656,23 +664,23 @@ public void testGetAllShellsWithDefaultClosedFilteredSpecificAssetIdsByTenantId( @Test public void testGetShellWithFilteredSpecificAssetIdsByTenantId() throws Exception { - - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); shellPayload.setSpecificAssetIds( null ); - SpecificAssetId asset1 = TestUtil.createSpecificAssetId( "CustomerPartId", "tenantTwoAssetIdValue", + SpecificAssetId asset1 = TestUtil.createSpecificAssetId( keyPrefix + "CustomerPartId", "tenantTwoAssetIdValue", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); - SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "CustomerPartId2", "tenantThreeAssetIdValue", + SpecificAssetId asset2 = TestUtil.createSpecificAssetId( keyPrefix + "CustomerPartId2", "tenantThreeAssetIdValue", List.of( jwtTokenFactory.tenantThree().getTenantId() ) ); - SpecificAssetId asset3 = TestUtil.createSpecificAssetId( "MaterialNumber", "withoutTenantAssetIdValue", + SpecificAssetId asset3 = TestUtil.createSpecificAssetId( keyPrefix + "MaterialNumber", "withoutTenantAssetIdValue", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); // Define specificAsset with wildcard which not allowed. (Only manufacturerPartId is defined in application.yml) - SpecificAssetId asset4 = TestUtil.createSpecificAssetId( "BPID", "ignoreWildcard", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset4 = TestUtil.createSpecificAssetId( keyPrefix + "BPID", "ignoreWildcard", List.of( getExternalSubjectIdWildcardPrefix() ) ); // Define specificAsset with wildcard which is allowed. (Only manufacturerPartId is defined in application.yml) - SpecificAssetId asset5 = TestUtil.createSpecificAssetId( "manufacturerPartId", "wildcardAllowed", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset5 = TestUtil.createSpecificAssetId( "manufacturerPartId", keyPrefix + "wildcardAllowed", + List.of( getExternalSubjectIdWildcardPrefix() ) ); shellPayload.setSpecificAssetIds( List.of( asset1, asset2, asset3, asset4, asset5 ) ); - - shellPayload.setId( UUID.randomUUID().toString() ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); String shellId = shellPayload.getId(); @@ -690,7 +698,7 @@ public void testGetShellWithFilteredSpecificAssetIdsByTenantId() throws Exceptio .andExpect( jsonPath( "$.id", equalTo( shellId ) ) ) .andExpect( jsonPath( "$.specificAssetIds[*].value", containsInAnyOrder( "tenantTwoAssetIdValue", "tenantThreeAssetIdValue", "withoutTenantAssetIdValue", "ignoreWildcard", - "wildcardAllowed" ) ) ); + keyPrefix + "wildcardAllowed" ) ) ); // test with tenant two mvc.perform( @@ -703,8 +711,10 @@ public void testGetShellWithFilteredSpecificAssetIdsByTenantId() throws Exceptio .andDo( MockMvcResultHandlers.print() ) .andExpect( status().isOk() ) .andExpect( jsonPath( "$.id", equalTo( shellId ) ) ) - .andExpect( jsonPath( "$.specificAssetIds[*].value", hasItems( "tenantTwoAssetIdValue", "withoutTenantAssetIdValue", "wildcardAllowed" ) ) ) - .andExpect( jsonPath( "$.specificAssetIds[*].value", not( hasItems( "tenantThreeAssetIdValue", "ignoreWildcard" ) ) ) ); + .andExpect( jsonPath( "$.specificAssetIds[*].value", + hasItems( "tenantTwoAssetIdValue", "withoutTenantAssetIdValue", keyPrefix + "wildcardAllowed" ) ) ) + .andExpect( jsonPath( "$.specificAssetIds[*].value", + not( hasItems( "tenantThreeAssetIdValue", "ignoreWildcard" ) ) ) ); } @Test @@ -888,32 +898,34 @@ public void testFindExternalShellIdsBySpecificAssetIdsWithTenantBasedVisibilityE @Test public void testFindExternalShellIdsBySpecificAssetIdsWithTenantBasedVisibilityAndWildcardExpectSuccess() throws Exception { // the keyPrefix ensures that this test can run against a persistent database multiple times - String keyPrefix = UUID.randomUUID().toString(); - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); shellPayload.setSpecificAssetIds( null ); - shellPayload.setId( UUID.randomUUID().toString() ); // asset1 is only visible for the owner because the externalSubjectId = null SpecificAssetId asset1 = TestUtil.createSpecificAssetId( keyPrefix + "defaultClosed", "value_1", null ); // asset2 is visible for everyone, because externalSubjectId = PUBLIC_READABLE and specificAssetKey is manufacturerPartId (which is in the list of allowedTypes via application.yml) - SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", "value_2", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", keyPrefix + "value_2", + List.of( getExternalSubjectIdWildcardPrefix() ) ); // asset3 is visible only for the owner, because externalSubjectId = PUBLIC_READABLE but specificAssetKey is bpId (which is not in the list of allowedTypes via application.yml) - SpecificAssetId asset3 = TestUtil.createSpecificAssetId( "bpId", "value_3", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset3 = TestUtil.createSpecificAssetId( keyPrefix + "bpId", "value_3", List.of( getExternalSubjectIdWildcardPrefix() ) ); // asset3 is visible for tenantTwo and tenantThree SpecificAssetId asset4 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo_tenantThree", "value_3", List.of( jwtTokenFactory.tenantTwo().getTenantId(), jwtTokenFactory.tenantThree().getTenantId() ) ); // asset4 is visible for tenantTwo, because externalSubjectId = tenantTwo - SpecificAssetId asset5 = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_private", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + SpecificAssetId asset5 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo", "value_2_private", + List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); shellPayload.setSpecificAssetIds( List.of( asset1, asset2, asset3, asset4, asset5 ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); SpecificAssetId sa1 = TestUtil.createSpecificAssetId( keyPrefix + "defaultClosed", "value_1", null ); - SpecificAssetId sa2 = TestUtil.createSpecificAssetId( "manufacturerPartId", "value_2", null ); - SpecificAssetId sa3 = TestUtil.createSpecificAssetId( "bpId", "value_3", null ); + SpecificAssetId sa2 = TestUtil.createSpecificAssetId( "manufacturerPartId", keyPrefix + "value_2", null ); + SpecificAssetId sa3 = TestUtil.createSpecificAssetId( keyPrefix + "bpId", "value_3", null ); SpecificAssetId sa4 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo_tenantThree", "value_3", null ); - SpecificAssetId sa5 = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_private", null ); + SpecificAssetId sa5 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo", "value_2_private", null ); String encodedSa1 = Base64.getUrlEncoder().encodeToString( serialize( sa1 ) ); String encodedSa2 = Base64.getUrlEncoder().encodeToString( serialize( sa2 ) ); @@ -1048,15 +1060,19 @@ public void testFindExternalShellIdsBySpecificAssetIdsWithDefaultClosedTenantBas @DisplayName( "Tenant based Shell visibility test" ) class TenantBasedShellVisibilityTest { + String keyPrefix; + @BeforeEach public void before() { shellRepository.deleteAll(); + keyPrefix = UUID.randomUUID().toString(); } @Test public void testGetAllShellsByOwningTenantId() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); - shellPayload.setId( UUID.randomUUID().toString() ); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); List shellpayloadSpecificAssetIDs = shellPayload.getSpecificAssetIds(); // Make all specificAssetIds to closed with externalSubjectId==null. shellpayloadSpecificAssetIDs.forEach( specificAssetId -> specificAssetId.setExternalSubjectId( null ) ); @@ -1098,17 +1114,19 @@ public void testGetAllShellsByOwningTenantId() throws Exception { @Test public void testGetAllShellsWithPublicAccessByTenantId() throws Exception { // the keyPrefix ensures that this test can run against a persistent database multiple times - String keyPrefix = UUID.randomUUID().toString(); - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); shellPayload.setSpecificAssetIds( null ); - shellPayload.setId( UUID.randomUUID().toString() ); // asset1 is only visible for the owner because the externalSubjectId = null SpecificAssetId asset1 = TestUtil.createSpecificAssetId( keyPrefix + "defaultClosed", "value_1", null ); // asset2 is visible for everyone, because externalSubjectId = PUBLIC_READABLE and specificAssetKey is manufacturerPartId (which is in the list of allowedTypes via application.yml) - SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", "value_2", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", keyPrefix + "value_2", + List.of( getExternalSubjectIdWildcardPrefix() ) ); // asset3 is visible for tenantTwo, because externalSubjectId = tenantTwo - SpecificAssetId asset3 = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_public", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + SpecificAssetId asset3 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo", "value_2_public", + List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); shellPayload.setSpecificAssetIds( List.of( asset1, asset2, asset3 ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); @@ -1154,8 +1172,9 @@ public void testGetAllShellsWithPublicAccessByTenantId() throws Exception { @Test public void testGetShellByExternalIdByOwningTenantId() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); - shellPayload.setId( UUID.randomUUID().toString() ); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); List shellpayloadSpecificAssetIDs = shellPayload.getSpecificAssetIds(); // Make all specificAssetIds to closed with externalSubjectId==null. shellpayloadSpecificAssetIDs.forEach( specificAssetId -> specificAssetId.setExternalSubjectId( null ) ); @@ -1194,17 +1213,19 @@ public void testGetShellByExternalIdByOwningTenantId() throws Exception { @Test public void testGetAllShellByExternalIdWithPublicAccessByTenantId() throws Exception { // the keyPrefix ensures that this test can run against a persistent database multiple times - String keyPrefix = UUID.randomUUID().toString(); - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( keyPrefix + "semanticId", "http://example.com/" ); + shellPayload.setId( keyPrefix ); shellPayload.setSpecificAssetIds( null ); - shellPayload.setId( UUID.randomUUID().toString() ); // asset1 is only visible for the owner because the externalSubjectId = null SpecificAssetId asset1 = TestUtil.createSpecificAssetId( keyPrefix + "defaultClosed", "value_1", null ); // asset2 is visible for everyone, because externalSubjectId = PUBLIC_READABLE and specificAssetKey is manufacturerPartId (which is in the list of allowedTypes via application.yml) - SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", "value_2", List.of( getExternalSubjectIdWildcardPrefix() ) ); + SpecificAssetId asset2 = TestUtil.createSpecificAssetId( "manufacturerPartId", keyPrefix + "value_2", + List.of( getExternalSubjectIdWildcardPrefix() ) ); // asset3 is visible for tenantTwo, because externalSubjectId = tenantTwo - SpecificAssetId asset3 = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_public", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + SpecificAssetId asset3 = TestUtil.createSpecificAssetId( keyPrefix + "tenantTwo", "value_2_public", + List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); shellPayload.setSpecificAssetIds( List.of( asset1, asset2, asset3 ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/GranularAssetAdministrationShellApiSecurityTest.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/GranularAssetAdministrationShellApiSecurityTest.java index 09397d41..6af09848 100644 --- a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/GranularAssetAdministrationShellApiSecurityTest.java +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/GranularAssetAdministrationShellApiSecurityTest.java @@ -20,20 +20,22 @@ package org.eclipse.tractusx.semantics.registry; -import static org.eclipse.tractusx.semantics.registry.TestUtil.getEncodedValue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import org.eclipse.tractusx.semantics.RegistryProperties; import org.eclipse.tractusx.semantics.aas.registry.model.AssetAdministrationShellDescriptor; import org.eclipse.tractusx.semantics.aas.registry.model.SpecificAssetId; -import org.eclipse.tractusx.semantics.aas.registry.model.SubmodelDescriptor; +import org.eclipse.tractusx.semantics.accesscontrol.sql.repository.AccessControlRuleRepository; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -48,8 +50,9 @@ @EnableConfigurationProperties( RegistryProperties.class ) public class GranularAssetAdministrationShellApiSecurityTest extends AssetAdministrationShellApiSecurityTest { - private static final String HTTP_EDC_DATA_PLANE_URL = "{\"submodelEndpointUrl\": \"http://edc-data-plane/url\"}"; - private static final String EXISTING_URL = "{\"submodelEndpointUrl\": \"http://endpoint-address\"}"; + private static final String HTTP_EDC_DATA_PLANE_URL_REQUEST = "{\"submodelEndpointUrl\": \"http://edc-data-plane/url\"}"; + private static final String EXISTING_URL = "http://endpoint-address"; + private static final String EXISTING_URL_REQUEST_FORMAT = "{\"submodelEndpointUrl\": \"%s\"}"; @Nested @DisplayName( "Authentication Tests" ) @@ -165,7 +168,7 @@ public void testRbacForLookupByAssetIds() throws Exception { class CustomAASApiTest extends AssetAdministrationShellApiSecurityTest.CustomAASApiTest { @Test - @Disabled("Test will be ignored, because the new api does not provided batch, fetch and query. This will be come later in version 0.3.1") + @Disabled( "Test will be ignored, because the new api does not provided batch, fetch and query. This will be come later in version 0.3.1" ) public void testRbacCreateShellInBatch() throws Exception { super.testRbacCreateShellInBatch(); } @@ -181,6 +184,9 @@ public void testRbacForFetchShellsByIds() throws Exception { @DisplayName( "Tenant based specificAssetId visibility test" ) class TenantBasedVisibilityTest extends AssetAdministrationShellApiSecurityTest.TenantBasedVisibilityTest { + @Autowired + private AccessControlRuleRepository accessControlRuleRepository; + @Test public void testGetAllShellsWithDefaultClosedFilteredSpecificAssetIdsByTenantId() throws Exception { super.testGetAllShellsWithDefaultClosedFilteredSpecificAssetIdsByTenantId(); @@ -188,11 +194,22 @@ public void testGetAllShellsWithDefaultClosedFilteredSpecificAssetIdsByTenantId( @Test public void testGetShellWithFilteredSpecificAssetIdsByTenantId() throws Exception { + accessControlRuleRepository.saveAllAndFlush( List.of( + TestUtil.createAccessRule( TestUtil.PUBLIC_READABLE, + Map.of( keyPrefix + "BPID", "ignoreWildcard", "manufacturerPartId", keyPrefix + "wildcardAllowed" ), + Set.of( "manufacturerPartId" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantTwo().getTenantId(), + Map.of( keyPrefix + "CustomerPartId", "tenantTwoAssetIdValue", keyPrefix + "MaterialNumber", "withoutTenantAssetIdValue" ), + Set.of( keyPrefix + "CustomerPartId", keyPrefix + "MaterialNumber" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantThree().getTenantId(), + Map.of( keyPrefix + "CustomerPartId2", "tenantThreeAssetIdValue" ), + Set.of( keyPrefix + "CustomerPartId2" ), Set.of( keyPrefix + "semanticId" ) ) + ) ); super.testGetShellWithFilteredSpecificAssetIdsByTenantId(); } @Test - @Disabled("Test will be ignored, because the new api does not provided batch, fetch and query. This will be come later in version 0.3.1") + @Disabled( "Test will be ignored, because the new api does not provided batch, fetch and query. This will be come later in version 0.3.1" ) public void testFetchShellsWithFilteredSpecificAssetIdsByTenantId() throws Exception { super.testFetchShellsWithFilteredSpecificAssetIdsByTenantId(); } @@ -209,6 +226,17 @@ public void testFindExternalShellIdsBySpecificAssetIdsWithTenantBasedVisibilityE @Test public void testFindExternalShellIdsBySpecificAssetIdsWithTenantBasedVisibilityAndWildcardExpectSuccess() throws Exception { + accessControlRuleRepository.saveAllAndFlush( List.of( + TestUtil.createAccessRule( TestUtil.PUBLIC_READABLE, + Map.of( keyPrefix + "bpId", "value_3", "manufacturerPartId", keyPrefix + "value_2" ), + Set.of( "manufacturerPartId" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantTwo().getTenantId(), + Map.of( keyPrefix + "tenantTwo_tenantThree", "value_3", keyPrefix + "tenantTwo", "value_2_private" ), + Set.of( keyPrefix + "tenantTwo_tenantThree", keyPrefix + "tenantTwo" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantThree().getTenantId(), + Map.of( keyPrefix + "tenantTwo_tenantThree", "value_3" ), + Set.of( keyPrefix + "tenantTwo_tenantThree" ), Set.of( keyPrefix + "semanticId" ) ) + ) ); super.testFindExternalShellIdsBySpecificAssetIdsWithTenantBasedVisibilityAndWildcardExpectSuccess(); } @@ -222,6 +250,9 @@ public void testFindExternalShellIdsBySpecificAssetIdsWithDefaultClosedTenantBas @DisplayName( "Tenant based Shell visibility test" ) class TenantBasedShellVisibilityTest extends AssetAdministrationShellApiSecurityTest.TenantBasedShellVisibilityTest { + @Autowired + private AccessControlRuleRepository accessControlRuleRepository; + @Test public void testGetAllShellsByOwningTenantId() throws Exception { super.testGetAllShellsByOwningTenantId(); @@ -229,6 +260,14 @@ public void testGetAllShellsByOwningTenantId() throws Exception { @Test public void testGetAllShellsWithPublicAccessByTenantId() throws Exception { + accessControlRuleRepository.saveAllAndFlush( List.of( + TestUtil.createAccessRule( TestUtil.PUBLIC_READABLE, + Map.of( "manufacturerPartId", keyPrefix + "value_2" ), + Set.of( "manufacturerPartId" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantTwo().getTenantId(), + Map.of( keyPrefix + "tenantTwo", "value_2_public" ), + Set.of( keyPrefix + "tenantTwo" ), Set.of( keyPrefix + "semanticId" ) ) + ) ); super.testGetAllShellsWithPublicAccessByTenantId(); } @@ -239,6 +278,14 @@ public void testGetShellByExternalIdByOwningTenantId() throws Exception { @Test public void testGetAllShellByExternalIdWithPublicAccessByTenantId() throws Exception { + accessControlRuleRepository.saveAllAndFlush( List.of( + TestUtil.createAccessRule( TestUtil.PUBLIC_READABLE, + Map.of( "manufacturerPartId", keyPrefix + "value_2" ), + Set.of( "manufacturerPartId" ), Set.of( keyPrefix + "semanticId" ) ), + TestUtil.createAccessRule( jwtTokenFactory.tenantTwo().getTenantId(), + Map.of( keyPrefix + "tenantTwo", "value_2_public" ), + Set.of( keyPrefix + "tenantTwo" ), Set.of() ) + ) ); super.testGetAllShellByExternalIdWithPublicAccessByTenantId(); } } @@ -272,13 +319,16 @@ public void testGetDescriptionReadRoleExpectUnauthorized() throws Exception { @DisplayName( "Submodel endpoint authorization Tests" ) class SubmodelEndpointAuthorizationApiTest { + @Autowired + private AccessControlRuleRepository accessControlRuleRepository; + @Test void testPostSubmodelDescriptorAuthorizedWithoutTokenExpectForbidden() throws Exception { mvc.perform( MockMvcRequestBuilders .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) - .content( HTTP_EDC_DATA_PLANE_URL ) + .content( HTTP_EDC_DATA_PLANE_URL_REQUEST ) .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantOne().getTenantId() ) ) .andDo( MockMvcResultHandlers.print() ) @@ -292,7 +342,7 @@ void testPostSubmodelDescriptorAuthorizedWithoutAppropriateRoleExpectForbidden() .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.readTwin() ) - .content( HTTP_EDC_DATA_PLANE_URL ) + .content( HTTP_EDC_DATA_PLANE_URL_REQUEST ) .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantOne().getTenantId() ) ) .andDo( MockMvcResultHandlers.print() ) @@ -319,7 +369,7 @@ void testPostSubmodelDescriptorAuthorizedWithoutTenantIdExpectForbidden() throws .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.tenantOne().submodelAccessControl() ) - .content( HTTP_EDC_DATA_PLANE_URL ) + .content( HTTP_EDC_DATA_PLANE_URL_REQUEST ) ) .andDo( MockMvcResultHandlers.print() ) .andExpect( status().isForbidden() ); @@ -332,7 +382,7 @@ void testPostSubmodelDescriptorAuthorizedWithoutAnyShellsExpectForbidden() throw .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.tenantOne().submodelAccessControl() ) - .content( HTTP_EDC_DATA_PLANE_URL ) + .content( HTTP_EDC_DATA_PLANE_URL_REQUEST ) .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantOne().getTenantId() ) ) .andDo( MockMvcResultHandlers.print() ) @@ -340,22 +390,33 @@ void testPostSubmodelDescriptorAuthorizedWithoutAnyShellsExpectForbidden() throw } @Test - @Disabled( "disabled while we have no way to create dynamic rules" ) void testPostSubmodelDescriptorAuthorizedWithoutMatchingSemanticIdExpectForbidden() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor( UUID.randomUUID().toString(), "http://endpoint-address" ); - shellPayload.setId( UUID.randomUUID().toString() ); + String randomId = UUID.randomUUID().toString(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( randomId + "semanticIdExample", EXISTING_URL + randomId ); + shellPayload.setSpecificAssetIds( null ); + shellPayload.setId( randomId ); - SpecificAssetId asset = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_private", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + String tenantTwoBpn = jwtTokenFactory.tenantTwo().getTenantId(); + SpecificAssetId asset = TestUtil.createSpecificAssetId( randomId + "tenantTwo", randomId + "value_2", List.of( tenantTwoBpn ) ); shellPayload.setSpecificAssetIds( List.of( asset ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); - //Tenant two should not have access due to the random specificAssetId + final var accessRule = TestUtil.createAccessRule( + tenantTwoBpn, + Map.of( randomId + "tenantTwo", randomId + "value_2" ), + Set.of( randomId + "tenantTwo" ), + Set.of() + ); + accessControlRuleRepository.saveAndFlush( accessRule ); + + //Tenant two should not have access because the rule does not give access to any semanticIds mvc.perform( MockMvcRequestBuilders .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.tenantTwo().submodelAccessControl() ) - .content( EXISTING_URL ) + .content( getRequestForUrl( EXISTING_URL + randomId ) ) .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantTwo().getTenantId() ) ) .andDo( MockMvcResultHandlers.print() ) @@ -364,55 +425,74 @@ void testPostSubmodelDescriptorAuthorizedWithoutMatchingSemanticIdExpectForbidde @Test void testPostSubmodelDescriptorAuthorizedWithMatchingShellAndSemanticIdExpectSuccess() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + String randomId = UUID.randomUUID().toString(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( randomId + "semanticIdExample", EXISTING_URL + randomId ); shellPayload.setSpecificAssetIds( null ); - shellPayload.setId( UUID.randomUUID().toString() ); + shellPayload.setId( randomId ); - SpecificAssetId asset = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_private", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + String tenantTwoBpn = jwtTokenFactory.tenantTwo().getTenantId(); + SpecificAssetId asset = TestUtil.createSpecificAssetId( randomId + "tenantTwo", randomId + "value_2", List.of( tenantTwoBpn ) ); shellPayload.setSpecificAssetIds( List.of( asset ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); - SubmodelDescriptor submodel = TestUtil.createSubmodel(); - performSubmodelCreateRequest( mapper.writeValueAsString( submodel ), getEncodedValue( shellPayload.getId() ) ); + final var accessRule = TestUtil.createAccessRule( + tenantTwoBpn, + Map.of( randomId + "tenantTwo", randomId + "value_2" ), + Set.of( randomId + "tenantTwo" ), + Set.of( randomId + "semanticIdExample" ) + ); + accessControlRuleRepository.saveAndFlush( accessRule ); - //Tenant two should have access due to the default semantic Id value + //Tenant two should have access due to the matching shell and semantic Id values mvc.perform( MockMvcRequestBuilders .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.tenantTwo().submodelAccessControl() ) - .content( EXISTING_URL ) - .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantTwo().getTenantId() ) + .content( getRequestForUrl( EXISTING_URL + randomId ) ) + .header( EXTERNAL_SUBJECT_ID_HEADER, tenantTwoBpn ) ) .andDo( MockMvcResultHandlers.print() ) .andExpect( status().isOk() ); } @Test - @Disabled( "disabled while we have no way to create dynamic rules" ) void testPostSubmodelDescriptorAuthorizedWithoutMatchingShellExpectForbidden() throws Exception { - AssetAdministrationShellDescriptor shellPayload = TestUtil.createCompleteAasDescriptor(); + String randomId = UUID.randomUUID().toString(); + AssetAdministrationShellDescriptor shellPayload = TestUtil + .createCompleteAasDescriptor( randomId + "semanticIdExample", EXISTING_URL + randomId ); shellPayload.setSpecificAssetIds( null ); - shellPayload.setId( UUID.randomUUID().toString() ); + shellPayload.setId( randomId ); - SpecificAssetId asset = TestUtil.createSpecificAssetId( "tenantTwo", "value_2_private", List.of( jwtTokenFactory.tenantTwo().getTenantId() ) ); + String tenantTwoBpn = jwtTokenFactory.tenantTwo().getTenantId(); + SpecificAssetId asset = TestUtil.createSpecificAssetId( randomId + "tenantTwo", randomId + "value_2", List.of( tenantTwoBpn ) ); shellPayload.setSpecificAssetIds( List.of( asset ) ); performShellCreateRequest( mapper.writeValueAsString( shellPayload ) ); - SubmodelDescriptor submodel = TestUtil.createSubmodel(); - performSubmodelCreateRequest( mapper.writeValueAsString( submodel ), getEncodedValue( shellPayload.getId() ) ); + final var accessRule = TestUtil.createAccessRule( + tenantTwoBpn, + Map.of( randomId + "tenantTwo", randomId + "value_2" ), + Set.of( randomId + "tenantTwo" ), + Set.of( randomId + "semanticIdExample" ) + ); + accessControlRuleRepository.saveAndFlush( accessRule ); - //Tenant three should have access due to the non-visible shell + //Tenant three should have access due to the non-visible shell (as it is only visible to tenantTwo mvc.perform( MockMvcRequestBuilders .post( "/api/v3.0/submodel-descriptor/authorized" ) .contentType( MediaType.APPLICATION_JSON ) .with( jwtTokenFactory.tenantThree().submodelAccessControl() ) - .content( EXISTING_URL ) + .content( getRequestForUrl( EXISTING_URL + randomId ) ) .header( EXTERNAL_SUBJECT_ID_HEADER, jwtTokenFactory.tenantThree().getTenantId() ) ) .andDo( MockMvcResultHandlers.print() ) .andExpect( status().isForbidden() ); } } + + private String getRequestForUrl( String url ) { + return String.format( EXISTING_URL_REQUEST_FORMAT, url ); + } } diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/TestUtil.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/TestUtil.java index 891b2e3c..b49d5675 100644 --- a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/TestUtil.java +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/TestUtil.java @@ -25,7 +25,10 @@ import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.tractusx.semantics.aas.registry.model.AssetAdministrationShellDescriptor; @@ -41,6 +44,10 @@ import org.eclipse.tractusx.semantics.aas.registry.model.ReferenceTypes; import org.eclipse.tractusx.semantics.aas.registry.model.SpecificAssetId; import org.eclipse.tractusx.semantics.aas.registry.model.SubmodelDescriptor; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRule; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.AccessRulePolicy; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.policy.AccessRulePolicyValue; +import org.eclipse.tractusx.semantics.accesscontrol.sql.model.policy.PolicyOperator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -48,6 +55,8 @@ public class TestUtil { + public static final String PUBLIC_READABLE = "PUBLIC_READABLE"; + public static AssetAdministrationShellDescriptor createCompleteAasDescriptor() { return createCompleteAasDescriptor( "semanticIdExample", "http://endpoint-address" ); } @@ -276,4 +285,30 @@ public static byte[] serialize( Object obj ) throws IOException { mapper.writeValue( os, obj ); return os.toByteArray(); } + + public static AccessRule createAccessRule( + String targetTenant, Map mandatorySpecificAssetIds, Set visibleSpecificAssetIds, Set visibleSemanticIds ) { + final AccessRulePolicy policy = new AccessRulePolicy(); + policy.setAccessRules( Set.of( + new AccessRulePolicyValue( AccessRulePolicy.BPN_RULE_NAME, PolicyOperator.EQUALS, targetTenant, null ), + new AccessRulePolicyValue( AccessRulePolicy.MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME, PolicyOperator.INCLUDES, null, + mandatorySpecificAssetIds.entrySet().stream() + .map( entry -> new AccessRulePolicyValue( entry.getKey(), PolicyOperator.EQUALS, entry.getValue(), null ) ) + .collect( Collectors.toSet() ) ), + new AccessRulePolicyValue( AccessRulePolicy.VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME, PolicyOperator.INCLUDES, null, + visibleSpecificAssetIds.stream() + .map( id -> new AccessRulePolicyValue( "name", PolicyOperator.EQUALS, id, null ) ) + .collect( Collectors.toSet() ) ), + new AccessRulePolicyValue( AccessRulePolicy.VISIBLE_SEMANTIC_IDS_RULE_NAME, PolicyOperator.INCLUDES, null, + visibleSemanticIds.stream() + .map( id -> new AccessRulePolicyValue( "modelUrn", PolicyOperator.EQUALS, id, null ) ) + .collect( Collectors.toSet() ) ) + ) ); + AccessRule accessRule = new AccessRule(); + accessRule.setTid( "owner" ); + accessRule.setTargetTenant( targetTenant ); + accessRule.setPolicyType( AccessRule.PolicyType.AAS ); + accessRule.setPolicy( policy ); + return accessRule; + } } \ No newline at end of file diff --git a/backend/src/test/resources/application-granular.yml b/backend/src/test/resources/application-granular.yml index 9072dba7..a451aee4 100644 --- a/backend/src/test/resources/application-granular.yml +++ b/backend/src/test/resources/application-granular.yml @@ -23,6 +23,4 @@ # The file must be named application.properties file. For whatever reason application.yml does not work. registry: - use-granular-access-control: true - -ACCESS_CONTROL_RULES_PATH: src/test/resources/test-access-rules.json \ No newline at end of file + use-granular-access-control: true \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8c828cd1..4f00bb92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -410,6 +410,8 @@ Authorization is supported by Role Based Access Control (RBAC). Following roles | update_digital_twin | Can update a digital twin. | | delete_digital_twin | Can delete a digital twin. | | submodel_access_control | Can perform submodel access control authorization calls. | +| read_access_rules | Can read the rules defined for access control. | +| write_access_rules | Can write the rules defined for access control. | Depending on being a Data Provider or a Data Consumer there are different tokens for authentication and authorization needed. #### Data Provider diff --git a/pom.xml b/pom.xml index 6e41755d..43e08140 100644 --- a/pom.xml +++ b/pom.xml @@ -204,6 +204,11 @@ jakarta.validation-api ${jakarta.validation.version} + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + jakarta.servlet jakarta.servlet-api @@ -300,12 +305,6 @@ ${junit.version} test - - org.hibernate.validator - hibernate-validator - 8.0.1.Final - test - org.apache.tomcat.embed tomcat-embed-el