diff --git a/openapi/openapi-component_catalog-v1.0.0.yaml b/openapi/openapi-component_catalog-v1.0.0.yaml index b0321b8..54f1b67 100644 --- a/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/openapi/openapi-component_catalog-v1.0.0.yaml @@ -394,6 +394,12 @@ paths: schema: type: string example: 'aSdFam...yCg==' + - name: X-Shared-Secret + in: header + required: false + schema: + type: string + example: 'SKiPJ...K46h' responses: "200": description: The CatalogItem. @@ -579,6 +585,12 @@ paths: required: true schema: type: string + - name: X-Shared-Secret + in: header + required: false + schema: + type: string + example: 'SKiPJ...K46h' responses: "200": description: A list of valid CatalogItems. @@ -639,6 +651,12 @@ paths: required: true schema: type: string + - name: X-Shared-Secret + in: header + required: false + schema: + type: string + example: 'SKiPJ...K46h' responses: "200": description: The CatalogItem. @@ -1215,6 +1233,10 @@ components: @com.fasterxml.jackson.annotation.JsonInclude( com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL ) + visible: + type: boolean + example: true + description: Whether the catalog item is visible for the user requesting it userActions: type: array @@ -1230,6 +1252,7 @@ components: - authors - date - updatedAt + - visible example: id: aSdFam...yCg== slug: myproject_some-repo @@ -1248,6 +1271,9 @@ components: - '@SomeAuthor' - '@SomeOtherAuthor' date: "2021-07-01T00:00:00Z" + updatedAt: 1625097600000 + componentCount: 5 + visible: true CatalogItemUserAction: properties: id: diff --git a/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java b/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java index a501e59..6019771 100644 --- a/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java +++ b/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java @@ -17,6 +17,12 @@ public class ApplicationPropertiesConfiguration { @Bean("basicAuthUsersConfig") + @ConfigurationProperties(prefix = "component-catalog.security.basic-auth") + public BasicAuthSecurityProps basicAuthSecurityProps() { + return BasicAuthSecurityProps.builder().build(); + } + + @Bean("securityConfig") @ConfigurationProperties(prefix = "component-catalog.security") public SecurityProps securityProps() { return SecurityProps.builder().build(); @@ -72,13 +78,19 @@ public CatalogItemUserActionGroupsRestrictionProps catalogItemGroupsRestrictionC @Bean("catalogItemDefaultConfig") @ConfigurationProperties(prefix = "catalog-item") - public CatalogItemDefaultProps catalogItemDefaultConfig() { + public CatalogItemDefaultProps catalogItemDefaultConfig() { return CatalogItemDefaultProps.builder().build(); } @Builder @Data public static class SecurityProps { + private String sharedSecret; + } + + @Builder + @Data + public static class BasicAuthSecurityProps { private UserProps provisioner; private UserProps cacheAdmin; } diff --git a/src/main/java/org/opendevstack/component_catalog/config/BasicAuthUsersConfiguration.java b/src/main/java/org/opendevstack/component_catalog/config/BasicAuthUsersConfiguration.java index 0c942c2..ffd3352 100644 --- a/src/main/java/org/opendevstack/component_catalog/config/BasicAuthUsersConfiguration.java +++ b/src/main/java/org/opendevstack/component_catalog/config/BasicAuthUsersConfiguration.java @@ -13,19 +13,19 @@ public class BasicAuthUsersConfiguration { @Bean public InMemoryUserDetailsManager userDetailsService( @Qualifier("basicAuthUsersConfig") - ApplicationPropertiesConfiguration.SecurityProps securityProps + ApplicationPropertiesConfiguration.BasicAuthSecurityProps basicAuthSecurityProps ) { UserDetails provisionerUser = User - .withUsername(securityProps.getProvisioner().getUsername()) - .password("{noop}" + securityProps.getProvisioner().getPassword()) - .roles(securityProps.getProvisioner().getRoles().toArray(String[]::new)) + .withUsername(basicAuthSecurityProps.getProvisioner().getUsername()) + .password("{noop}" + basicAuthSecurityProps.getProvisioner().getPassword()) + .roles(basicAuthSecurityProps.getProvisioner().getRoles().toArray(String[]::new)) .build(); UserDetails cacheAdminUser = User - .withUsername(securityProps.getCacheAdmin().getUsername()) - .password("{noop}" + securityProps.getCacheAdmin().getPassword()) - .roles(securityProps.getCacheAdmin().getRoles().toArray(String[]::new)) + .withUsername(basicAuthSecurityProps.getCacheAdmin().getUsername()) + .password("{noop}" + basicAuthSecurityProps.getCacheAdmin().getPassword()) + .roles(basicAuthSecurityProps.getCacheAdmin().getRoles().toArray(String[]::new)) .build(); return new InMemoryUserDetailsManager( diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiController.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiController.java index 5d0c23f..c1429ec 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiController.java +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiController.java @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration.SecurityProps; import org.opendevstack.component_catalog.server.api.CatalogItemsApi; import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException; import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException; @@ -31,6 +32,7 @@ public class CatalogItemsApiController implements CatalogItemsApi { private final AuthorizationInfo authInfo; private final CatalogItemsApiFacade catalogItemsApiFacade; private final AuthenticationFacade authenticationFacade; + private final SecurityProps securityProps; @Override public ResponseEntity> getCatalogItems(SortOrder sortByTitle, String catalogId) { @@ -52,7 +54,7 @@ public ResponseEntity> getCatalogItems(SortOrder sortByTitle, @Override public ResponseEntity> getCatalogItemsForProjectKey(String catalogId, SortOrder sortByTitle, - String projectKey) { + String projectKey, String xSharedSecret) { log.debug("User '{}' requested catalog items for catalog id and projectKey: '{}', '{}'", authInfo.getCurrentPrincipalName(), catalogId, projectKey); try { @@ -63,6 +65,7 @@ public ResponseEntity> getCatalogItemsForProjectKey(String cat .sortOrder(sortByTitle) .projectKey(projectKey) .accessToken(accessToken) + .ignoreVisibilityRestrictions(securityProps.getSharedSecret().equals(xSharedSecret)) .build(); var items = catalogItemsApiFacade.fetchCatalogItems(catalogItemRequestParams); @@ -74,11 +77,12 @@ public ResponseEntity> getCatalogItemsForProjectKey(String cat } @Override - public ResponseEntity getCatalogItemById(String id) { + public ResponseEntity getCatalogItemById(String id, String xSharedSecret) { log.debug("User '{}' requested catalog item with id: '{}'", authInfo.getCurrentPrincipalName(), id); try { var catalogRequestParams = CatalogRequestParams.builder() .catalogItemId(id) + .ignoreVisibilityRestrictions(securityProps.getSharedSecret().equals(xSharedSecret)) .build(); var catItem = catalogItemsApiFacade.fetchCatalogItem(catalogRequestParams); if (catItem == null) { @@ -93,7 +97,7 @@ public ResponseEntity getCatalogItemById(String id) { } @Override - public ResponseEntity getCatalogItemByIdForProjectKey(String id, String projectKey) { + public ResponseEntity getCatalogItemByIdForProjectKey(String id, String projectKey, String xSharedSecret) { log.debug("User '{}' requested catalog item with id and projectKey: '{}', '{}'", authInfo.getCurrentPrincipalName(), id, projectKey); try { @@ -103,6 +107,7 @@ public ResponseEntity getCatalogItemByIdForProjectKey(String id, St .catalogItemId(id) .projectKey(projectKey) .accessToken(accessToken) + .ignoreVisibilityRestrictions(securityProps.getSharedSecret().equals(xSharedSecret)) .build(); var catItem = catalogItemsApiFacade.fetchCatalogItem(catalogRequestParams); if (catItem == null) { diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogRequestParams.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogRequestParams.java index 0473c2c..a9e8c9a 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogRequestParams.java +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogRequestParams.java @@ -26,4 +26,6 @@ public class CatalogRequestParams { @Builder.Default String projectKey = Strings.EMPTY; String accessToken; + @Builder.Default + boolean ignoreVisibilityRestrictions = false; } diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacade.java index 0b2ff79..b8d5b1a 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacade.java @@ -11,30 +11,17 @@ import org.opendevstack.component_catalog.server.model.CatalogDescriptor; import org.opendevstack.component_catalog.server.model.CatalogItem; import org.opendevstack.component_catalog.server.model.CatalogItemFilter; -import org.opendevstack.component_catalog.server.org.opendevstack.component_catalog.server.model.wrapper.CatalogItemWrapper; import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.*; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntity; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntityMetadata; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntityPermissionEnum; -import org.opendevstack.component_catalog.server.services.catalog.CatalogServiceAdapter; -import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException; -import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException; +import org.opendevstack.component_catalog.server.services.catalog.*; import org.opendevstack.component_catalog.server.services.exceptions.InvalidEntityException; import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException; -import org.opendevstack.component_catalog.server.services.filters.CatalogItemsFilter; import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; import org.opendevstack.component_catalog.server.services.slug.CatalogItemSlug; import org.opendevstack.component_catalog.util.JwtUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -54,13 +41,9 @@ public class CatalogItemsApiFacade { private final CatalogsCollectionService catalogsCollectionService; private final OdsApiServerServiceProps odsApiServerServiceProps; private final ProjectComponentsService projectComponentsService; - private final RolesWhitelistedService rolesWhitelistedService; - private final ProvisionerActionsService provisionerActionsService; private final AuthenticationFacade authenticationFacade; - private final List catalogItemFilters; - public CatalogItem asCatalogItem(CatalogRequestParams catalogRequestParams) { var tokenizedCatalogRequestParams = tokenize(catalogRequestParams); return asCatalogItemWithoutMandatoryToken(tokenizedCatalogRequestParams); @@ -165,7 +148,7 @@ private List fetchCatalogItemsByCatalogId(CatalogRequestParams cata ) ) .filter(Objects::nonNull) - .filter(item -> applyFilters(item, catalogRequestParams.getProjectKey())) + .filter(item -> applyVisibilityFilter(item, catalogRequestParams.isIgnoreVisibilityRestrictions())) .sorted(fieldSorter(CatalogItem::getTitle, catalogRequestParams.getSortOrder())) .toList(); else { @@ -188,7 +171,7 @@ public CatalogItem fetchCatalogItem(CatalogRequestParams catalogRequestParams) .build() ) ) - .filter(item -> applyFilters(item, catalogRequestParams.getProjectKey())) + .filter(item -> applyVisibilityFilter(item, catalogRequestParams.isIgnoreVisibilityRestrictions())) .orElse(null); } @@ -214,12 +197,8 @@ public CatalogItem fetchCatalogItemBySlug(CatalogItemSlug slug) ); } - // We can not do workflowsFilter in here, because the parameters merge was already done - protected boolean applyFilters(CatalogItem item, String projectKey) { - var params = Collections.singletonList(projectKey); - - return catalogItemFilters.stream() - .allMatch(filter -> filter.filter(item, params)); + protected boolean applyVisibilityFilter(CatalogItem item, boolean ignoreVisibilityRestrictions) { + return ignoreVisibilityRestrictions || item.getVisible(); } protected boolean filterByContributingFileExists(String id) { diff --git a/src/main/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapper.java b/src/main/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapper.java index 7b17648..7adea8b 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapper.java +++ b/src/main/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapper.java @@ -15,6 +15,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.openapitools.jackson.nullable.JsonNullable; +import org.opendevstack.component_catalog.server.services.filters.CatalogItemsFilter; import org.springframework.stereotype.Component; import java.util.*; @@ -31,6 +32,7 @@ @AllArgsConstructor public class EntitiesMapper { private final CatalogItemUserActionMapper catalogItemUserActionMapper; + private final List catalogItemFilters; public Catalog asCatalog(CatalogEntityContext catalogEntityCtx) { log.debug("Mapping CatalogEntityContext to Catalog: {}", catalogEntityCtx); @@ -89,6 +91,15 @@ public CatalogItemWrapper asCatalogItem(CatalogItemEntityContext catalogItemEnti ? CatalogItemSlug.normalise(catalogProject) + CatalogItemSlug.SEPARATOR + catalogItemEntityCtx.getRepoCatalogItemPathAt().getRepoSlug() : null; + var isVisible = catalogItemFilters.stream() + .allMatch(filter -> + filter.filter( + CatalogItem.builder() + .id(catalogItemEntityCtx.getId()) + .restrictions(catalogItemRestrictions) + .build(), + List.of(projectKey))); + var catalogItem = CatalogItem.builder() .id(catalogItemEntityCtx.getId()) .slug(itemSlug) @@ -104,6 +115,7 @@ public CatalogItemWrapper asCatalogItem(CatalogItemEntityContext catalogItemEnti .userActions(catalogItemUserActions) .restrictions(catalogItemRestrictions) .componentCount(componentCount) + .visible(isVisible) .build(); log.debug("Resulting CatalogItem: {}", catalogItem); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c18066f..045ec93 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,16 +34,18 @@ user-actions: default-message-definition-id: ${USER_ACTIONS_DEFAULT_MESSAGE_DEFINITION_ID} component-catalog: security: - provisioner: - username: ${SECURITY_PROVISIONER_USERNAME} - password: ${SECURITY_PROVISIONER_PASSWORD} - roles: - - PROVISIONER - cache-admin: - username: ${SECURITY_CACHE_ADMIN_USERNAME} - password: ${SECURITY_CACHE_ADMIN_PASSWORD} - roles: - - CACHE_ADMIN + shared-secret: ${SECURITY_SHARED_SECRET} + basic-auth: + provisioner: + username: ${SECURITY_PROVISIONER_USERNAME} + password: ${SECURITY_PROVISIONER_PASSWORD} + roles: + - PROVISIONER + cache-admin: + username: ${SECURITY_CACHE_ADMIN_USERNAME} + password: ${SECURITY_CACHE_ADMIN_PASSWORD} + roles: + - CACHE_ADMIN caching: bitbucket-service-cache: enabled: true diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogApiAdapterTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogApiAdapterTest.java index c277c72..2a31b65 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogApiAdapterTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogApiAdapterTest.java @@ -3,6 +3,7 @@ import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_catalog.server.mappers.CatalogItemUserActionMapper; import org.opendevstack.component_catalog.server.mappers.CatalogItemUserActionParameterMapper; @@ -36,6 +37,7 @@ import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityContextMother; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityMother; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityUserAction; +import org.opendevstack.component_catalog.server.services.filters.CatalogItemsFilter; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluatorResultMother; @@ -84,7 +86,9 @@ void setUp() { List.of(dummyEvaluator), groupsRestrictionProps ); - var entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper); + CatalogItemsFilter filter = Mockito.mock(CatalogItemsFilter.class); + + var entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper, List.of(filter)); catalogApiAdapter = new CatalogApiAdapter( entitiesMapper, diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiControllerTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiControllerTest.java index 6be2510..d3cd529 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiControllerTest.java @@ -1,10 +1,12 @@ package org.opendevstack.component_catalog.server.controllers; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration.SecurityProps; import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException; import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException; @@ -23,8 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CatalogItemsApiControllerTest { @@ -46,9 +47,21 @@ class CatalogItemsApiControllerTest { @Mock private AuthenticationFacade authenticationFacade; - @InjectMocks private CatalogItemsApiController catalogItemsApiController; + @BeforeEach + void setUp() { + var securityProps = SecurityProps.builder() + .sharedSecret("test-shared-secret") + .build(); + catalogItemsApiController = new CatalogItemsApiController( + authInfo, + catalogItemsApiFacade, + authenticationFacade, + securityProps + ); + } + @Test void givenValidCatalogId_WhenGetCatalogItems_ThenReturnItemsList() throws InvalidIdException { @@ -105,7 +118,7 @@ void givenValidProjectKey_WhenGetCatalogItemsForProjectKey_ThenReturnItemsList() when(catalogItemsApiFacade.fetchCatalogItems(any())).thenReturn(List.of(item)); // When - var response = catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, SortOrder.ASC, projectKey); + var response = catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, SortOrder.ASC, projectKey, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -126,7 +139,8 @@ void givenInvalidProjectKey_WhenGetCatalogItemsForProjectKey_ThenThrowBadRequest () -> catalogItemsApiController.getCatalogItemsForProjectKey( catalogId, SortOrder.ASC, - invalidProjectKey + invalidProjectKey, + null ) ) .isInstanceOf(BadRequestException.class) @@ -139,7 +153,7 @@ void givenEmptyResult_WhenGetCatalogItemsForProjectKey_ThenReturnEmptyList() thr when(authenticationFacade.getAccessToken()).thenReturn("access-token"); // When - var response = catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, SortOrder.ASC, projectKey); + var response = catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, SortOrder.ASC, projectKey, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -147,6 +161,35 @@ void givenEmptyResult_WhenGetCatalogItemsForProjectKey_ThenReturnEmptyList() thr assertThat(response.getBody()).isEmpty(); } + @Test + void givenMatchingSharedSecret_WhenGetCatalogItemsForProjectKey_ThenIgnoreVisibilityRestrictions() + throws InvalidCatalogEntityException, InvalidIdException { + when(authenticationFacade.getAccessToken()).thenReturn("access-token"); + + catalogItemsApiController.getCatalogItemsForProjectKey( + catalogId, + SortOrder.ASC, + projectKey, + "test-shared-secret" + ); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItems(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isTrue(); + } + + @Test + void givenInvalidSharedSecret_WhenGetCatalogItemsForProjectKey_ThenDoNotIgnoreVisibilityRestrictions() + throws InvalidCatalogEntityException, InvalidIdException { + when(authenticationFacade.getAccessToken()).thenReturn("access-token"); + + catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, SortOrder.ASC, projectKey, "invalid-secret"); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItems(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isFalse(); + } + @Test void givenValidCatalogItemId_WhenGetCatalogItemById_ThenReturnCatalogItem() throws InvalidIdException, InvalidCatalogEntityException { @@ -165,7 +208,7 @@ void givenValidCatalogItemId_WhenGetCatalogItemById_ThenReturnCatalogItem() // When - var response = catalogItemsApiController.getCatalogItemById(catalogItemId); + var response = catalogItemsApiController.getCatalogItemById(catalogItemId, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -180,7 +223,7 @@ void givenInvalidCatalogItemId_WhenGetCatalogItemById_ThenThrowRestEntityNotFoun .thenThrow(new InvalidIdException("Invalid ID")); // When / Then - assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemById(invalidCatalogItemId)) + assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemById(invalidCatalogItemId, null)) .isInstanceOf(RestEntityNotFoundException.class) .hasMessageContaining("Catalog item not found"); } @@ -193,7 +236,7 @@ void givenInvalidCatalogItemEntity_WhenGetCatalogItemById_ThenThrowInvalidRestEn .thenThrow(new InvalidCatalogItemEntityException("Invalid ID")); // When / Then - assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemById(invalidCatalogItemId)) + assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemById(invalidCatalogItemId, null)) .isInstanceOf(InvalidRestEntityException.class) .hasMessageContaining("Invalid catalog item"); } @@ -203,13 +246,33 @@ void givenCatalogItemNotFound_WhenGetCatalogItemById_ThenReturnNotFound() throws when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); when(catalogItemsApiFacade.fetchCatalogItem(any())).thenReturn(null); // When - var response = catalogItemsApiController.getCatalogItemById(catalogItemId); + var response = catalogItemsApiController.getCatalogItemById(catalogItemId, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getBody()).isNull(); } + @Test + void givenMatchingSharedSecret_WhenGetCatalogItemById_ThenIgnoreVisibilityRestrictions() + throws InvalidCatalogItemEntityException, InvalidIdException { + catalogItemsApiController.getCatalogItemById(catalogItemId, "test-shared-secret"); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItem(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isTrue(); + } + + @Test + void givenInvalidSharedSecret_WhenGetCatalogItemById_ThenDoNotIgnoreVisibilityRestrictions() + throws InvalidCatalogItemEntityException, InvalidIdException { + catalogItemsApiController.getCatalogItemById(catalogItemId, "invalid-secret"); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItem(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isFalse(); + } + @Test void givenValidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenReturnItem() throws InvalidIdException, InvalidCatalogItemEntityException { @@ -220,7 +283,7 @@ void givenValidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenReturnItem() when(catalogItemsApiFacade.fetchCatalogItem(any())).thenReturn(catalogItem); // When - var response = catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey); + var response = catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -236,7 +299,7 @@ void givenInvalidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenThrowRestEnti // When / Then assertThatThrownBy( - () -> catalogItemsApiController.getCatalogItemByIdForProjectKey(invalidCatalogId, projectKey) + () -> catalogItemsApiController.getCatalogItemByIdForProjectKey(invalidCatalogId, projectKey, null) ) .isInstanceOf(RestEntityNotFoundException.class) .hasMessageContaining("Catalog item not found"); @@ -252,7 +315,7 @@ void givenInvalidCatalogItemEntity_WhenGetCatalogItemByIdForProjectKey_ThenThrow // When / Then - assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey)) + assertThatThrownBy(() -> catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey, null)) .isInstanceOf(InvalidRestEntityException.class) .hasMessageContaining("Invalid catalog item"); } @@ -265,11 +328,35 @@ void givenCatalogItemNotFound_WhenGetCatalogItemByIdForProjectKey_ThenReturnNotF when(catalogItemsApiFacade.fetchCatalogItem(any())).thenReturn(null); // When - var response = catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey); + var response = catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogId, projectKey, null); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getBody()).isNull(); } + @Test + void givenMatchingSharedSecret_WhenGetCatalogItemByIdForProjectKey_ThenIgnoreVisibilityRestrictions() + throws InvalidCatalogItemEntityException, InvalidIdException { + when(authenticationFacade.getAccessToken()).thenReturn("access-token"); + + catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogItemId, projectKey, "test-shared-secret"); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItem(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isTrue(); + } + + @Test + void givenInvalidSharedSecret_WhenGetCatalogItemByIdForProjectKey_ThenDoNotIgnoreVisibilityRestrictions() + throws InvalidCatalogItemEntityException, InvalidIdException { + when(authenticationFacade.getAccessToken()).thenReturn("access-token"); + + catalogItemsApiController.getCatalogItemByIdForProjectKey(catalogItemId, projectKey, "invalid-secret"); + + var requestParams = ArgumentCaptor.forClass(CatalogRequestParams.class); + verify(catalogItemsApiFacade).fetchCatalogItem(requestParams.capture()); + assertThat(requestParams.getValue().isIgnoreVisibilityRestrictions()).isFalse(); + } + } \ No newline at end of file diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacadeTest.java index 8df1ec2..3de4c7f 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacadeTest.java @@ -23,13 +23,7 @@ import org.opendevstack.component_catalog.server.org.opendevstack.component_catalog.server.model.wrapper.CatalogItemWrapper; import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.*; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntity; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntityMetadata; -import org.opendevstack.component_catalog.server.services.catalog.CatalogEntityPermissionEnum; -import org.opendevstack.component_catalog.server.services.catalog.CatalogServiceAdapter; -import org.opendevstack.component_catalog.server.services.catalog.CatalogsCollectionsEntity; -import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException; -import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException; +import org.opendevstack.component_catalog.server.services.catalog.*; import org.opendevstack.component_catalog.server.services.catalog.business.UserActionsEntity; import org.opendevstack.component_catalog.server.services.catalog.business.UserActionsEntityMother; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityContext; @@ -42,11 +36,7 @@ import org.opendevstack.component_catalog.server.services.slug.CatalogItemSlug; import org.opendevstack.component_catalog.util.JwtUtils; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -326,7 +316,7 @@ void GivenContributingFileExists_WhenFetchCatalogItems_ThenMapsFiltersSortsAndRe doReturn(permissions).when(catalogItemsApiFacade).currentPrincipalCatalogPermissions(catalogId); doReturn(true).when(catalogItemsApiFacade).filterByContributingFileExists(anyString()); - doReturn(true).when(catalogItemsApiFacade).applyFilters(any(), eq(projectKey)); + doReturn(true).when(catalogItemsApiFacade).applyVisibilityFilter(any(), anyBoolean()); CatalogItem itemB = new CatalogItem(); itemB.setId("B"); @@ -366,7 +356,7 @@ void GivenContributingFileExists_WhenFetchCatalogItems_ThenMapsFiltersSortsAndRe assertThat(result.get(1).getId()).isEqualTo("B"); verify(catalogApiAdapter, times(2)).asCatalogItem(any(), anyList(), anyList(), any()); - verify(catalogItemsApiFacade, times(2)).applyFilters(any(), eq(projectKey)); + verify(catalogItemsApiFacade, times(2)).applyVisibilityFilter(any(), anyBoolean()); verify(catalogItemsApiFacade).filterByContributingFileExists(catalogId); } } @@ -458,7 +448,7 @@ void GivenMixedItems_WhenFetchCatalogItems_ThenReturnsOnlyProjectMatchingItems() doAnswer(inv -> { CatalogItem it = inv.getArgument(0); return "keep".equals(it.getId()); - }).when(catalogItemsApiFacade).applyFilters(any(CatalogItem.class), eq(projectKey)); + }).when(catalogItemsApiFacade).applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); var params = CatalogRequestParams.builder() .catalogId(catalogId) @@ -475,7 +465,7 @@ void GivenMixedItems_WhenFetchCatalogItems_ThenReturnsOnlyProjectMatchingItems() assertThat(result.getFirst().getId()).isEqualTo("keep"); verify(catalogApiAdapter, times(2)).asCatalogItem(any(), anyList(), anyList(), any()); - verify(catalogItemsApiFacade, times(2)).applyFilters(any(CatalogItem.class), eq(projectKey)); + verify(catalogItemsApiFacade, times(2)).applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); verify(catalogItemsApiFacade, times(0)).filterByContributingFileExists("keep"); } } @@ -596,7 +586,7 @@ void GivenNoCatalogId_WhenFetchCatalogItems_ThenFetchesItemsFromAllCatalogs() th doReturn(true).when(catalogItemsApiFacade) .filterByContributingFileExists(anyString()); doReturn(true).when(catalogItemsApiFacade) - .applyFilters(any(), any()); + .applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); CatalogItem item1 = new CatalogItem(); item1.setId("item-1"); @@ -664,7 +654,7 @@ void GivenCatalogId_WhenFetchCatalogItems_ThenFetchesOnlyItemsFromThatCatalog() .filterByContributingFileExists(catalogId); doReturn(true).when(catalogItemsApiFacade) - .applyFilters(any(), any()); + .applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); CatalogItem item = new CatalogItem(); item.setId("item-1"); @@ -740,7 +730,7 @@ void GivenCatalogIdAndRepoReadPermission_WhenFetchCatalogItems_ThenPassesRepoRea doReturn(true) .when(catalogItemsApiFacade) - .applyFilters(any(), any()); + .applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); when(catalogApiAdapter.asCatalogItem( any(), @@ -818,7 +808,7 @@ void GivenCatalogIdAndAccessToken_WhenFetchCatalogItems_ThenUsesSameTokenForMapp doReturn(true) .when(catalogItemsApiFacade) - .applyFilters(any(), any()); + .applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); when(catalogApiAdapter.asCatalogItem( any(), @@ -909,7 +899,7 @@ void GivenNoCatalogId_WhenFetchCatalogItems_ThenSortsAllItemsAscendingByTitle() doReturn(true) .when(catalogItemsApiFacade) - .applyFilters(any(), any()); + .applyVisibilityFilter(any(CatalogItem.class), anyBoolean()); // when var result = catalogItemsApiFacade.fetchCatalogItems(params); @@ -938,7 +928,7 @@ void GivenExistingEntityAndPassingFilters_WhenFetchCatalogItem_ThenReturnsCatalo item.setTitle("X"); doReturn(item).when(catalogItemsApiFacade).asCatalogItem(any(CatalogRequestParams.class)); - doReturn(true).when(catalogItemsApiFacade).applyFilters(item, projectKey); + doReturn(true).when(catalogItemsApiFacade).applyVisibilityFilter(eq(item), anyBoolean()); var params = CatalogRequestParams.builder() .catalogItemId(catalogItemId) @@ -954,7 +944,7 @@ void GivenExistingEntityAndPassingFilters_WhenFetchCatalogItem_ThenReturnsCatalo verify(catalogEntitiesService, times(1)).getCatalogItemEntity(catalogItemId); verify(catalogItemsApiFacade, times(1)).asCatalogItem(any(CatalogRequestParams.class)); - verify(catalogItemsApiFacade, times(1)).applyFilters(item, projectKey); + verify(catalogItemsApiFacade, times(1)).applyVisibilityFilter(eq(item), anyBoolean()); verify(catalogItemsApiFacade, times(0)).filterByContributingFileExists(catalogItemId); } @@ -994,7 +984,7 @@ void GivenItemFilteredOutByProject_WhenFetchCatalogItem_ThenReturnsNull() item.setId(catalogItemId); doReturn(item).when(catalogItemsApiFacade).asCatalogItem(any(CatalogRequestParams.class)); - doReturn(false).when(catalogItemsApiFacade).applyFilters(item, projectKey); + doReturn(false).when(catalogItemsApiFacade).applyVisibilityFilter(eq(item), anyBoolean()); var params = CatalogRequestParams.builder() .catalogItemId(catalogItemId) @@ -1007,7 +997,27 @@ void GivenItemFilteredOutByProject_WhenFetchCatalogItem_ThenReturnsNull() // then assertThat(response).isNull(); - verify(catalogItemsApiFacade, times(1)).applyFilters(item, projectKey); + verify(catalogItemsApiFacade, times(1)).applyVisibilityFilter(eq(item), anyBoolean()); + } + + @Test + void GivenInvisibleItemAndVisibilityRestrictions_WhenApplyingVisibilityFilter_ThenReturnFalse() { + var item = new CatalogItem(); + item.setVisible(false); + + var result = catalogItemsApiFacade.applyVisibilityFilter(item, false); + + assertThat(result).isFalse(); + } + + @Test + void GivenInvisibleItemAndIgnoredVisibilityRestrictions_WhenApplyingVisibilityFilter_ThenReturnTrue() { + var item = new CatalogItem(); + item.setVisible(false); + + var result = catalogItemsApiFacade.applyVisibilityFilter(item, true); + + assertThat(result).isTrue(); } @Test diff --git a/src/test/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapperTest.java b/src/test/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapperTest.java index 8237666..492303e 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapperTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/mappers/EntitiesMapperTest.java @@ -3,6 +3,7 @@ import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.openapitools.jackson.nullable.JsonNullable; import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_catalog.server.model.CatalogItemUserAction; @@ -16,6 +17,7 @@ import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityContextMother; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityUserActionMother; import org.opendevstack.component_catalog.server.services.catalog.entity.UserActionRestrictionsMother; +import org.opendevstack.component_catalog.server.services.filters.CatalogItemsFilter; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluatorResultMother; @@ -31,6 +33,7 @@ class EntitiesMapperTest { private CatalogItemUserActionParameterMapper catalogItemUserActionParameterMapper; + private CatalogItemsFilter catalogItemsFilter; private EntitiesMapper entitiesMapper; @BeforeEach @@ -50,8 +53,9 @@ void setUp() { List.of(dummyEvaluator), groupsRestrictionProps ); + catalogItemsFilter = Mockito.mock(CatalogItemsFilter.class); - this.entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper); + this.entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper, List.of(catalogItemsFilter)); } @Test @@ -101,6 +105,40 @@ void asCatalogItem_mapsCatalogItemEntityContextToCatalogItem() { assertThat(catalogItem.catalogItem().getUserActions()).hasSize(2); } + @Test + void givenCatalogItemAcceptedByFilters_whenAsCatalogItem_thenItemIsVisible() { + var catalogItemEntityCtx = CatalogItemEntityContextMother.of(); + var projectKey = "PROJECT-1"; + Mockito.when(catalogItemsFilter.filter(Mockito.any(), Mockito.eq(List.of(projectKey)))).thenReturn(true); + + var catalogItem = entitiesMapper.asCatalogItem( + catalogItemEntityCtx, + Collections.emptyList(), + Collections.emptyList(), + projectKey, + null + ); + + assertThat(catalogItem.catalogItem().getVisible()).isTrue(); + } + + @Test + void givenCatalogItemRejectedByFilters_whenAsCatalogItem_thenItemIsNotVisible() { + var catalogItemEntityCtx = CatalogItemEntityContextMother.of(); + var projectKey = "PROJECT-1"; + Mockito.when(catalogItemsFilter.filter(Mockito.any(), Mockito.eq(List.of(projectKey)))).thenReturn(false); + + var catalogItem = entitiesMapper.asCatalogItem( + catalogItemEntityCtx, + Collections.emptyList(), + Collections.emptyList(), + projectKey, + null + ); + + assertThat(catalogItem.catalogItem().getVisible()).isFalse(); + } + @Test void asCatalogItemUserAction_mapsUserActionEntityToCatalogItemUserAction() { var baseUserActionEntity = UserActionEntityMother.of(); diff --git a/src/test/java/org/opendevstack/component_catalog/server/services/UserActionsEntitiesServiceTest.java b/src/test/java/org/opendevstack/component_catalog/server/services/UserActionsEntitiesServiceTest.java index e614d0d..81c1689 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/services/UserActionsEntitiesServiceTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/services/UserActionsEntitiesServiceTest.java @@ -1,5 +1,11 @@ package org.opendevstack.component_catalog.server.services; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_catalog.server.mappers.CatalogItemUserActionMapper; import org.opendevstack.component_catalog.server.mappers.CatalogItemUserActionParameterMapper; @@ -16,13 +22,8 @@ import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityUserAction; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityUserActionMother; import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException; +import org.opendevstack.component_catalog.server.services.filters.CatalogItemsFilter; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluator; -import org.apache.commons.lang3.tuple.Pair; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsEvaluatorResultMother; import org.springframework.test.util.ReflectionTestUtils; @@ -40,6 +41,9 @@ class UserActionsEntitiesServiceTest { @Mock private CatalogServiceAdapter catalogServiceAdapter; + @Mock + private List catalogItemsFilters; + private UserActionsEntitiesService userActionsEntitiesService; @BeforeEach @@ -55,7 +59,7 @@ void setUp() { var catalogItemUserActionMapper = new CatalogItemUserActionMapper(new CatalogItemUserActionParameterMapper(), List.of(dummyEvaluator), groupsRestrictionProps); - var entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper); + var entitiesMapper = new EntitiesMapper(catalogItemUserActionMapper, catalogItemsFilters); this.userActionsEntitiesService = new UserActionsEntitiesService(catalogServiceAdapter, entitiesMapper); ReflectionTestUtils.setField(userActionsEntitiesService, "defaultUserActionsBitbucketId", "testId"); diff --git a/src/test/resources/application-testing.env b/src/test/resources/application-testing.env index 5f7ce32..6fcbe50 100644 --- a/src/test/resources/application-testing.env +++ b/src/test/resources/application-testing.env @@ -18,6 +18,8 @@ SECURITY_PROVISIONER_PASSWORD=provisioner-password SECURITY_CACHE_ADMIN_USERNAME=cache-admin-username SECURITY_CACHE_ADMIN_PASSWORD=cache-admin-password +SECURITY_SHARED_SECRET=test-shared-secret + USER_ACTIONS_DEFAULT_MESSAGE_DEFINITION_ID="DEFAULT_ERROR" PROJECTS_INFO_SERVICE_BASE_REST_URL="https://projects-info-service-devstack-qa.openshift/v1"