From cdff066de5a7c6f8593be877b144d074618ca5b0 Mon Sep 17 00:00:00 2001 From: Istvan Zoltan Nagy Date: Wed, 21 Feb 2024 13:04:07 +0100 Subject: [PATCH] Shell lookups must return shells ordered in a deterministic way - Modifies repository and service implementation to fetch AAS Ids ordered by the createdDate (and then the AAS Id as secondary criteria) - Fixes a bug in list all API - Makes fetch size configurable - Adds new configuration property to charts - Adds new tests to cover the changes better --- CHANGELOG.md | 6 + .../semantics/RegistryProperties.java | 4 + .../repository/ShellIdentifierRepository.java | 54 +++-- .../registry/repository/ShellRepository.java | 4 + .../registry/service/ShellService.java | 142 +++++++----- backend/src/main/resources/application.yml | 1 + .../service/GranularShellServiceTest.java | 112 +++++++++ .../service/LegacyShellServiceTest.java | 214 ++++++++++++++++++ charts/registry/Chart.yaml | 2 +- .../templates/registry/registry-secret.yaml | 1 + charts/registry/values.yaml | 1 + 11 files changed, 474 insertions(+), 67 deletions(-) create mode 100644 backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/GranularShellServiceTest.java create mode 100644 backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/LegacyShellServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c3c1cd..35b2361f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.4.0 +### Added +- Granular access control + +## fixed + ## 0.3.23 ### Added diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java b/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java index fb092813..4218ef66 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java @@ -53,6 +53,10 @@ public class RegistryProperties { * This flag turns on the granular access control logic if set to true. */ private Boolean useGranularAccessControl; + /** + * Configures the number of records fetched in one batch when a page of shells is requested. + */ + private Integer granularAccessControlFetchSize; /** * Properties for Identity Management system diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellIdentifierRepository.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellIdentifierRepository.java index 47d0908b..fee8b96a 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellIdentifierRepository.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellIdentifierRepository.java @@ -20,6 +20,7 @@ package org.eclipse.tractusx.semantics.registry.repository; +import java.time.Instant; import java.util.List; import java.util.Set; import java.util.UUID; @@ -27,6 +28,7 @@ import org.eclipse.tractusx.semantics.registry.model.Shell; import org.eclipse.tractusx.semantics.registry.model.ShellIdentifier; import org.eclipse.tractusx.semantics.registry.model.projection.ShellIdentifierMinimal; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -41,19 +43,36 @@ public interface ShellIdentifierRepository extends JpaRepository findByShellId( Shell shellId ); @Query( value = """ - SELECT NEW org.eclipse.tractusx.semantics.registry.model.projection.ShellIdentifierMinimal(sid.shellId.idExternal, sid.key, sid.value) + SELECT s.id + FROM ShellIdentifier sid + JOIN sid.shellId s + WHERE + CONCAT( sid.key, sid.value ) IN ( :keyValueCombinations ) + AND ( + s.createdDate > :cutoffDate + OR ( s.createdDate = :cutoffDate AND s.idExternal > :cursorValue ) + ) + GROUP BY s.id, s.createdDate, s.idExternal + HAVING COUNT(*) = :keyValueCombinationsSize + ORDER BY s.createdDate ASC, s.idExternal ASC + """ ) + List findAPageOfShellIdsBySpecificAssetIds( + List keyValueCombinations, int keyValueCombinationsSize, Instant cutoffDate, String cursorValue, Pageable pageable ); + + @Query( value = """ + SELECT NEW org.eclipse.tractusx.semantics.registry.model.projection.ShellIdentifierMinimal(s.idExternal, sid.key, sid.value) FROM ShellIdentifier sid + JOIN sid.shellId s WHERE - sid.shellId.id IN ( - SELECT filtersid.shellId.id - FROM ShellIdentifier filtersid - WHERE - CONCAT(filtersid.key, filtersid.value) IN (:keyValueCombinations) - GROUP BY filtersid.shellId.id - HAVING COUNT(*) = :keyValueCombinationsSize - ) + s.id IN ( :shellIds ) + AND ( + s.createdDate > :cutoffDate + OR ( s.createdDate = :cutoffDate AND s.idExternal > :cursorValue ) + ) + ORDER BY s.createdDate ASC, s.idExternal ASC """ ) - List findMinimalShellIdsBySpecificAssetIds( List keyValueCombinations, int keyValueCombinationsSize ); + List findMinimalShellIdsByShellIds( + List shellIds, Instant cutoffDate, String cursorValue ); /** * Returns external shell ids for the given keyValueCombinations. @@ -74,7 +93,11 @@ HAVING COUNT(*) = :keyValueCombinationsSize FROM shell s JOIN shell_identifier si ON s.id = si.fk_shell_id WHERE - CONCAT(si.namespace, si.identifier) IN (:keyValueCombinations) + CONCAT( si.namespace, si.identifier ) IN ( :keyValueCombinations ) + AND ( + s.created_date > :cutoffDate + OR ( s.created_date = :cutoffDate AND s.id_external > :cursorValue ) + ) AND ( :tenantId = :owningTenantId OR si.namespace = :globalAssetId @@ -90,8 +113,10 @@ OR EXISTS ( AND sies.FK_SHELL_IDENTIFIER_EXTERNAL_SUBJECT_ID = si.id ) ) - GROUP BY s.id_external + GROUP BY s.id_external, s.created_date HAVING COUNT(*) = :keyValueCombinationsSize + ORDER BY s.created_date, s.id_external + LIMIT :pageSize """, nativeQuery = true ) List findExternalShellIdsByIdentifiersByExactMatch( @Param( "keyValueCombinations" ) List keyValueCombinations, @Param( "keyValueCombinationsSize" ) int keyValueCombinationsSize, @@ -99,5 +124,8 @@ List findExternalShellIdsByIdentifiersByExactMatch( @Param( "keyValueCom @Param( "publicWildcardPrefix" ) String publicWildcardPrefix, @Param( "publicWildcardAllowedTypes" ) List publicWildcardAllowedTypes, @Param( "owningTenantId" ) String owningTenantId, - @Param( "globalAssetId" ) String globalAssetId ); + @Param( "globalAssetId" ) String globalAssetId, + @Param( "cutoffDate" ) Instant cutoffDate, + @Param( "cursorValue" ) String cursorValue, + @Param( "pageSize" ) int pageSize); } diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellRepository.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellRepository.java index 33d4d7e2..9bf5202f 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellRepository.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/repository/ShellRepository.java @@ -20,6 +20,7 @@ package org.eclipse.tractusx.semantics.registry.repository; +import java.time.Instant; import java.util.List; import java.util.Optional; import java.util.Set; @@ -142,4 +143,7 @@ s.id IN ( ) """) List findAllBySubmodelEndpointAddress( String endpointAddress ); + + @Query("SELECT s.createdDate FROM Shell s WHERE s.idExternal = :idExternal") + Optional getCreatedDateByIdExternal( String idExternal ); } diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/service/ShellService.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/service/ShellService.java index b1b0ff8d..4a7a86df 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/service/ShellService.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/service/ShellService.java @@ -22,6 +22,9 @@ import static org.springframework.data.domain.PageRequest.ofSize; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; @@ -56,6 +59,7 @@ import org.eclipse.tractusx.semantics.registry.utils.ShellSpecification; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -73,7 +77,11 @@ public class ShellService { public static final String DUPLICATE_SUBMODEL_ID_SHORT_EXCEPTION = "An AssetAdministration Submodel for the given IdShort does already exists."; private static final String SORT_FIELD_NAME_SHELL = "createdDate"; private static final String SORT_FIELD_NAME_SUBMODEL = "id"; + private static final String DEFAULT_EXTERNAL_ID = "00000000-0000-0000-0000-000000000000"; + private static final Instant MINIMUM_SQL_DATETIME = OffsetDateTime + .of( 1800, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC ).toInstant(); private static final int MAXIMUM_RECORDS = 1000; + private static final int DEFAULT_FETCH_SIZE = 500; private final ShellRepository shellRepository; private final ShellIdentifierRepository shellIdentifierRepository; @@ -82,6 +90,7 @@ public class ShellService { private final String owningTenantId; private final String externalSubjectIdWildcardPrefix; private final List externalSubjectIdWildcardAllowedTypes; + private final int granularAccessControlFetchSize; public ShellService( ShellRepository shellRepository, ShellIdentifierRepository shellIdentifierRepository, @@ -95,6 +104,7 @@ public ShellService( ShellRepository shellRepository, this.owningTenantId = registryProperties.getIdm().getOwningTenantId(); this.externalSubjectIdWildcardPrefix = registryProperties.getExternalSubjectIdWildcardPrefix(); this.externalSubjectIdWildcardAllowedTypes = registryProperties.getExternalSubjectIdWildcardAllowedTypes(); + this.granularAccessControlFetchSize = Optional.ofNullable( registryProperties.getGranularAccessControlFetchSize() ).orElse( DEFAULT_FETCH_SIZE ); } @Transactional @@ -199,16 +209,19 @@ public ShellCollectionDto findAllShells( Integer pageSize, String cursorVal, Str ShellCursor cursor = new ShellCursor( pageSize, cursorVal ); var specification = shellAccessHandler.shellFilterSpecification( SORT_FIELD_NAME_SHELL, cursor, externalSubjectId ); final var foundList = new ArrayList(); - boolean hasNext = true; //fetch 1 more item to make sure there is a visible item for the next page - int fetchSize = pageSize + 1; - while ( foundList.size() < fetchSize && hasNext ) { - Page currentPage = shellRepository.findAll( specification, ofSize( fetchSize ) ); + while ( foundList.size() < pageSize + 1 ) { + Page currentPage = shellRepository.findAll( specification, ofSize( granularAccessControlFetchSize ) ); List shells = shellAccessHandler.filterListOfShellProperties( currentPage.stream().toList(), externalSubjectId ); shells.stream() - .limit( (long) fetchSize - foundList.size() ) + .limit( (long) pageSize + 1 - foundList.size() ) .forEach( foundList::add ); - hasNext = currentPage.hasNext(); + if ( !currentPage.hasNext() ) { + break; + } + ShellCursor shellCursor = new ShellCursor( pageSize, + cursor.getEncodedCursorShell( lastItemOf( currentPage.getContent() ).getCreatedDate(), currentPage.hasNext() ) ); + specification = shellAccessHandler.shellFilterSpecification( SORT_FIELD_NAME_SHELL, shellCursor, externalSubjectId ); } String nextCursor = null; @@ -261,35 +274,17 @@ public GetAllAssetAdministrationShellIdsByAssetLink200Response findExternalShell Integer pageSize, String cursor, String externalSubjectId ) { pageSize = getPageSize( pageSize ); + final String cursorValue = getCursorDecoded( cursor ).orElse( DEFAULT_EXTERNAL_ID ); try { - List keyValueCombinations = shellIdentifiers.stream().map( shellIdentifier -> shellIdentifier.getKey() + shellIdentifier.getValue() ).toList(); - - //TODO: if we can define a sorting order, we should consider doing it on the database side - final List assetIdList = new ArrayList<>(); - String nextCursor; + final List visibleAssetIds; if ( shellAccessHandler.supportsGranularAccessControl() ) { - List queryResults = shellIdentifierRepository - .findMinimalShellIdsBySpecificAssetIds( keyValueCombinations, keyValueCombinations.size() ); - Set userQuery = shellIdentifiers.stream() - .map( id -> new SpecificAssetId( id.getKey(), id.getValue() ) ) - .collect( Collectors.toSet() ); - List allVisible = shellAccessHandler.filterToVisibleShellIdsForLookup( userQuery, queryResults, externalSubjectId ); - allVisible.stream() - .skip( getCursorDecoded( cursor, allVisible ) ) - .limit( pageSize ) - .forEach( assetIdList::add ); - nextCursor = getCursorEncoded( allVisible, assetIdList ); + visibleAssetIds = fetchAPageOfAasIdsUsingGranularAccessControl( shellIdentifiers, externalSubjectId, cursorValue, pageSize ); } else { - List queryResult = shellIdentifierRepository.findExternalShellIdsByIdentifiersByExactMatch( keyValueCombinations, - keyValueCombinations.size(), externalSubjectId, externalSubjectIdWildcardPrefix, externalSubjectIdWildcardAllowedTypes, owningTenantId, - ShellIdentifier.GLOBAL_ASSET_ID_KEY ); - pageSize = getPageSize( pageSize ); - - int startIndex = getCursorDecoded( cursor, queryResult ); - queryResult.stream().skip( startIndex ).limit( pageSize ).forEach( assetIdList::add ); - - nextCursor = getCursorEncoded( queryResult, assetIdList ); + visibleAssetIds = fetchAPageOfAasIdsUsingLegacyAccessControl( shellIdentifiers, externalSubjectId, cursorValue, pageSize ); } + + final var assetIdList = visibleAssetIds.stream().limit( pageSize ).toList(); + final String nextCursor = getCursorEncoded( visibleAssetIds, assetIdList ); final var response = new GetAllAssetAdministrationShellIdsByAssetLink200Response(); response.setResult( assetIdList ); response.setPagingMetadata( new PagedResultPagingMetadata().cursor( nextCursor ) ); @@ -301,32 +296,50 @@ public GetAllAssetAdministrationShellIdsByAssetLink200Response findExternalShell } } - private String getCursorEncoded( List queryResult, List assetIdList ) { - if ( !queryResult.isEmpty() ) { - if ( !assetIdList.get( assetIdList.size() - 1 ).equals( queryResult.get( queryResult.size() - 1 ) ) ) { - String lastEle = assetIdList.get( assetIdList.size() - 1 ); - return Base64.getEncoder().encodeToString( lastEle.getBytes() ); + private List fetchAPageOfAasIdsUsingLegacyAccessControl( + Set shellIdentifiers, String externalSubjectId, String cursorValue, int pageSize ) { + final var fetchSize = pageSize + 1; + final Instant cutoffDate = shellRepository.getCreatedDateByIdExternal( cursorValue ) + .orElse( MINIMUM_SQL_DATETIME ); + List keyValueCombinations = toKeyValueCombinations( shellIdentifiers ); + return shellIdentifierRepository.findExternalShellIdsByIdentifiersByExactMatch( keyValueCombinations, + keyValueCombinations.size(), externalSubjectId, externalSubjectIdWildcardPrefix, externalSubjectIdWildcardAllowedTypes, owningTenantId, + ShellIdentifier.GLOBAL_ASSET_ID_KEY, cutoffDate, cursorValue, fetchSize ); + } + + private List fetchAPageOfAasIdsUsingGranularAccessControl( + Set shellIdentifiers, String externalSubjectId, String cursorValue, int pageSize ) + throws DenyAccessException { + Set userQuery = shellIdentifiers.stream() + .map( id -> new SpecificAssetId( id.getKey(), id.getValue() ) ) + .collect( Collectors.toSet() ); + List keyValueCombinations = toKeyValueCombinations( shellIdentifiers ); + final var fetchSize = granularAccessControlFetchSize; + + String currentCursorValue = cursorValue; + final List visibleAssetIds = new ArrayList<>(); + while ( visibleAssetIds.size() < pageSize + 1 ) { + final Instant currentCutoffDate = shellRepository.getCreatedDateByIdExternal( currentCursorValue ) + .orElse( MINIMUM_SQL_DATETIME ); + List shellIds = shellIdentifierRepository.findAPageOfShellIdsBySpecificAssetIds( + keyValueCombinations, keyValueCombinations.size(), currentCutoffDate, currentCursorValue, PageRequest.ofSize( fetchSize ) ); + if ( shellIds.isEmpty() ) { + break; } - } - return null; - } + List queryResults = shellIdentifierRepository + .findMinimalShellIdsByShellIds( shellIds, currentCutoffDate, currentCursorValue ); - private String getCursorDecoded( String cursor ) { - return Optional.ofNullable( cursor ) - .map( Base64.getDecoder()::decode ) - .map( String::new ) - .orElse( null ); - } - - private int getCursorDecoded( String cursor, List queryResult ) { - return Optional.ofNullable( getCursorDecoded( cursor ) ) - .map( decodedValue -> queryResult.indexOf( decodedValue ) + 1 ) - .orElse( 0 ); + shellAccessHandler.filterToVisibleShellIdsForLookup( userQuery, queryResults, externalSubjectId ).stream() + .limit( (long) fetchSize - visibleAssetIds.size() ) + .forEach( visibleAssetIds::add ); + currentCursorValue = lastItemOf( queryResults ).shellId(); + } + return visibleAssetIds; } @Transactional( readOnly = true ) public List findExternalShellIdsByIdentifiersByAnyMatch( Set shellIdentifiers, String externalSubjectId ) { - List keyValueCombinations = shellIdentifiers.stream().map( shellIdentifier -> shellIdentifier.getKey() + shellIdentifier.getValue() ).toList(); + List keyValueCombinations = toKeyValueCombinations( shellIdentifiers ); return shellRepository.findExternalShellIdsByIdentifiersByAnyMatch( keyValueCombinations, @@ -386,7 +399,7 @@ public Set save( String externalShellId, Set s return ImmutableSet.copyOf( shellIdentifierRepository.saveAll( identifiersToUpdate ) ); } - private static void mapShellIdentifier( Stream identifiersToUpdate ) { + private void mapShellIdentifier( Stream identifiersToUpdate ) { identifiersToUpdate.filter( identifiers -> !identifiers.getKey().equalsIgnoreCase( "globalAssetId" ) ).forEach( identifier -> { if ( identifier.getSemanticId() != null ) { @@ -494,7 +507,7 @@ public List saveBatch( List shells ) { } ).collect( Collectors.toList() ); } - @Transactional(readOnly = true) + @Transactional( readOnly = true ) public boolean hasAccessToShellWithVisibleSubmodelEndpoint( String endpointAddress, String externalSubjectId ) { List shells = shellRepository.findAllBySubmodelEndpointAddress( endpointAddress ); List filtered = shellAccessHandler.filterListOfShellProperties( shells, externalSubjectId ); @@ -509,4 +522,27 @@ private Shell doFindShellByExternalIdWithoutFiltering( String externalShellId ) return shellRepository.findByIdExternal( externalShellId ) .orElseThrow( () -> new EntityNotFoundException( String.format( "Shell for identifier %s not found", externalShellId ) ) ); } + + private T lastItemOf( List list ) { + return list.get( list.size() - 1 ); + } + + private List toKeyValueCombinations( Set shellIdentifiers ) { + return shellIdentifiers.stream() + .map( shellIdentifier -> shellIdentifier.getKey() + shellIdentifier.getValue() ) + .toList(); + } + + private String getCursorEncoded( List queryResult, List assetIdList ) { + if ( !queryResult.isEmpty() && !lastItemOf( assetIdList ).equals( lastItemOf( queryResult ) ) ) { + return Base64.getEncoder().encodeToString( lastItemOf( assetIdList ).getBytes() ); + } + return null; + } + + private Optional getCursorDecoded( String cursor ) { + return Optional.ofNullable( cursor ) + .map( Base64.getDecoder()::decode ) + .map( String::new ); + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3e449267..d6605325 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -66,6 +66,7 @@ registry: idm: public-client-id: catenax-portal use-granular-access-control: false + granular-access-control-fetch-size: 500 springdoc: cache: diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/GranularShellServiceTest.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/GranularShellServiceTest.java new file mode 100644 index 00000000..faad7a40 --- /dev/null +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/GranularShellServiceTest.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * 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.registry.service; + +import java.util.Set; + +import org.eclipse.tractusx.semantics.RegistryProperties; +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 org.eclipse.tractusx.semantics.accesscontrol.sql.repository.AccessControlRuleRepository; +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; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles( profiles = { "granular", "test" } ) +@EnableConfigurationProperties( RegistryProperties.class ) +class GranularShellServiceTest extends LegacyShellServiceTest { + + private static final String TENANT_ONE = "TENANT_ONE"; + + @Autowired + private AccessControlRuleRepository accessControlRuleRepository; + + @Test + void testsLookupWithNoMatchingRecordsExpectEmptyListAndNoCursor() { + createRule(); + super.testsLookupWithNoMatchingRecordsExpectEmptyListAndNoCursor(); + } + + @Test + void testsLookupWithLessThanAPageOfMatchingRecordsExpectPartialListAndNoCursor() { + createRule(); + super.testsLookupWithLessThanAPageOfMatchingRecordsExpectPartialListAndNoCursor(); + } + + @Test + void testsLookupWithExactlyOnePageOfMatchingRecordsExpectFullListAndNoCursor() { + createRule(); + super.testsLookupWithExactlyOnePageOfMatchingRecordsExpectFullListAndNoCursor(); + } + + @Test + void testsLookupWithOneMoreThanOnePageOfMatchingRecordsExpectFullListAndCursor() { + createRule(); + super.testsLookupWithOneMoreThanOnePageOfMatchingRecordsExpectFullListAndCursor(); + } + + @Test + void testsLookupWithTwoPagesOfMatchingRecordsExpectFullListAndCursor() { + createRule(); + super.testsLookupWithTwoPagesOfMatchingRecordsExpectFullListAndCursor(); + } + + @Test + void testsLookupWithThreePagesOfMatchingRecordsRequestingSecondPageExpectFullListAndCursor() { + createRule(); + super.testsLookupWithThreePagesOfMatchingRecordsRequestingSecondPageExpectFullListAndCursor(); + } + + @Test + void testsLookupWithThreePagesOfMatchingRecordsRequestingPageOfOnlyLastItemExpectSingleItemAndNoCursor() { + createRule(); + super.testsLookupWithThreePagesOfMatchingRecordsRequestingPageOfOnlyLastItemExpectSingleItemAndNoCursor(); + } + + private void createRule() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + AccessRulePolicy policy = new AccessRulePolicy(); + policy.setAccessRules( Set.of( + new AccessRulePolicyValue( AccessRulePolicy.BPN_RULE_NAME, PolicyOperator.EQUALS, TENANT_TWO, null ), + new AccessRulePolicyValue( AccessRulePolicy.MANDATORY_SPECIFIC_ASSET_IDS_RULE_NAME, PolicyOperator.INCLUDES, null, Set.of( + new AccessRulePolicyValue( specificAssetIdName, PolicyOperator.EQUALS, specificAssetIdValue, null ) + ) ), + new AccessRulePolicyValue( AccessRulePolicy.VISIBLE_SPECIFIC_ASSET_ID_NAMES_RULE_NAME, PolicyOperator.INCLUDES, null, Set.of( + new AccessRulePolicyValue( "name", PolicyOperator.EQUALS, specificAssetIdName, null ) + ) ), + new AccessRulePolicyValue( AccessRulePolicy.VISIBLE_SEMANTIC_IDS_RULE_NAME, PolicyOperator.INCLUDES, null, Set.of() ) + ) ); + AccessRule accessRule = new AccessRule(); + accessRule.setPolicyType( AccessRule.PolicyType.AAS ); + accessRule.setTid( TENANT_ONE ); + accessRule.setTargetTenant( TENANT_TWO ); + accessRule.setPolicy( policy ); + accessControlRuleRepository.save( accessRule ); + } +} \ No newline at end of file diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/LegacyShellServiceTest.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/LegacyShellServiceTest.java new file mode 100644 index 00000000..88c43f4f --- /dev/null +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/service/LegacyShellServiceTest.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * 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.registry.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.IntStream; + +import org.eclipse.tractusx.semantics.RegistryProperties; +import org.eclipse.tractusx.semantics.aas.registry.model.AssetAdministrationShellDescriptor; +import org.eclipse.tractusx.semantics.registry.TestUtil; +import org.eclipse.tractusx.semantics.registry.mapper.ShellMapper; +import org.eclipse.tractusx.semantics.registry.model.Shell; +import org.eclipse.tractusx.semantics.registry.model.ShellIdentifier; +import org.junit.jupiter.api.BeforeEach; +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; + +@SpringBootTest +@AutoConfigureMockMvc +@EnableConfigurationProperties( RegistryProperties.class ) +class LegacyShellServiceTest { + + protected static final String TENANT_TWO = "TENANT_TWO"; + + @Autowired + private ShellService shellService; + @Autowired + private ShellMapper shellMapper; + protected String keyPrefix; + + @BeforeEach + void setUp() { + keyPrefix = UUID.randomUUID().toString(); + } + + @Test + void testsLookupWithNoMatchingRecordsExpectEmptyListAndNoCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, null, null, TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().isEmpty(); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNull(); + } + + @Test + void testsLookupWithLessThanAPageOfMatchingRecordsExpectPartialListAndNoCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + String id = UUID.randomUUID().toString(); + createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, 5, null, TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( 1 ).contains( id ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNull(); + } + + @Test + void testsLookupWithExactlyOnePageOfMatchingRecordsExpectFullListAndNoCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + List expectedIds = IntStream.rangeClosed( 0, 4 ) + .mapToObj( i -> UUID.randomUUID().toString() ) + .toList(); + expectedIds.forEach( id -> createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, 5, null, TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( expectedIds.size() ).containsAll( expectedIds ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNull(); + } + + @Test + void testsLookupWithOneMoreThanOnePageOfMatchingRecordsExpectFullListAndCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + int pageSize = 5; + int totalItems = pageSize + 1; + List expectedIds = IntStream.range( 0, totalItems ) + .mapToObj( i -> UUID.randomUUID().toString() ) + .toList(); + expectedIds.forEach( id -> createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, pageSize, null, TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( pageSize ).containsAll( expectedIds.subList( 0, pageSize ) ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNotNull() + .isEqualTo( toCursor( expectedIds, pageSize - 1 ) ); + } + + @Test + void testsLookupWithTwoPagesOfMatchingRecordsExpectFullListAndCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + int pageSize = 5; + int totalItems = pageSize + pageSize; + List expectedIds = IntStream.range( 0, totalItems ) + .mapToObj( i -> UUID.randomUUID().toString() ) + .toList(); + expectedIds.forEach( id -> createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, pageSize, null, TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( pageSize ).containsAll( expectedIds.subList( 0, pageSize ) ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNotNull() + .isEqualTo( toCursor( expectedIds, pageSize - 1 ) ); + } + + @Test + void testsLookupWithThreePagesOfMatchingRecordsRequestingSecondPageExpectFullListAndCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + int pageSize = 5; + int totalItems = pageSize * 3; + List expectedIds = IntStream.range( 0, totalItems ) + .mapToObj( i -> UUID.randomUUID().toString() ) + .toList(); + expectedIds.forEach( id -> createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, pageSize, toCursor( expectedIds, pageSize - 1 ), TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( pageSize ).containsAll( expectedIds.subList( pageSize, pageSize * 2 ) ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNotNull() + .isEqualTo( toCursor( expectedIds, pageSize * 2 - 1 ) ); + } + + @Test + void testsLookupWithThreePagesOfMatchingRecordsRequestingPageOfOnlyLastItemExpectSingleItemAndNoCursor() { + String specificAssetIdName = keyPrefix + "key"; + String specificAssetIdValue = "value"; + Set criteria = Set.of( new ShellIdentifier().withKey( specificAssetIdName ).withValue( specificAssetIdValue ) ); + int pageSize = 5; + int totalItems = pageSize * 3; + List expectedIds = IntStream.range( 0, totalItems ) + .mapToObj( i -> UUID.randomUUID().toString() ) + .toList(); + expectedIds.forEach( id -> createShellWithIdAndSpecificAssetIds( id, specificAssetIdName, specificAssetIdValue ) ); + + final var actual = shellService.findExternalShellIdsByIdentifiersByExactMatch( + criteria, pageSize, toCursor( expectedIds, pageSize * 3 - 2 ), TENANT_TWO ); + + assertThat( actual ).isNotNull(); + assertThat( actual.getResult() ).isNotNull().hasSize( 1 ).containsAll( expectedIds.subList( pageSize * 3 - 1, pageSize * 3 ) ); + assertThat( actual.getPagingMetadata() ).isNotNull(); + assertThat( actual.getPagingMetadata().getCursor() ).isNull(); + } + + private String toCursor( List expectedIds, int indexOfLastVisibleId ) { + return new String( Base64.getUrlEncoder().encode( expectedIds.get( indexOfLastVisibleId ).getBytes() ) ); + } + + private void createShellWithIdAndSpecificAssetIds( String id, String specificAssetIdName, String specificAssetIdValue ) { + AssetAdministrationShellDescriptor shellDescriptor = TestUtil.createCompleteAasDescriptor(); + shellDescriptor.setId( id ); + shellDescriptor.setSpecificAssetIds( List.of( TestUtil.createSpecificAssetId( specificAssetIdName, specificAssetIdValue, List.of( TENANT_TWO ) ) ) ); + Shell shell = shellMapper.fromApiDto( shellDescriptor ); + shellService.mapShellCollection( shell ); + if ( !shell.getSubmodels().isEmpty() ) + shellService.mapSubmodel( shell.getSubmodels() ); + shellService.save( shell ); + } +} \ No newline at end of file diff --git a/charts/registry/Chart.yaml b/charts/registry/Chart.yaml index 5a9949a4..488ac4e5 100644 --- a/charts/registry/Chart.yaml +++ b/charts/registry/Chart.yaml @@ -26,7 +26,7 @@ sources: - https://github.com/eclipse-tractusx/sldt-digital-twin-registry type: application -version: 0.4.7 +version: 0.4.8 appVersion: 0.3.23 dependencies: diff --git a/charts/registry/templates/registry/registry-secret.yaml b/charts/registry/templates/registry/registry-secret.yaml index 0996c02b..05f4fa27 100644 --- a/charts/registry/templates/registry/registry-secret.yaml +++ b/charts/registry/templates/registry/registry-secret.yaml @@ -40,4 +40,5 @@ data: REGISTRY_EXTERNAL_SUBJECT_ID_WILDCARD_PREFIX: {{ .Values.registry.externalSubjectIdWildcardPrefix | b64enc }} REGISTRY_EXTERNAL_SUBJECT_ID_WILDCARD_ALLOWED_TYPES: {{ .Values.registry.externalSubjectIdWildcardAllowedTypes | b64enc }} REGISTRY_USE_GRANULAR_ACCESS_CONTROL: {{ .Values.registry.useGranularAccessControl | b64enc }} + REGISTRY_GRANULAR_ACCESS_CONTROL_FETCH_SIZE: {{ .Values.registry.granularAccessControlFetchSize | b64enc }} diff --git a/charts/registry/values.yaml b/charts/registry/values.yaml index 8e325841..61dcfb22 100644 --- a/charts/registry/values.yaml +++ b/charts/registry/values.yaml @@ -50,6 +50,7 @@ registry: externalSubjectIdWildcardPrefix: PUBLIC_READABLE externalSubjectIdWildcardAllowedTypes: manufacturerPartId,digitalTwinType useGranularAccessControl: "false" + granularAccessControlFetchSize: "500" service: port: 8080 type: ClusterIP