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 2e44d1a..b83cca3 100644 --- a/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java +++ b/src/main/java/org/opendevstack/component_catalog/config/ApplicationPropertiesConfiguration.java @@ -34,6 +34,12 @@ public ExternalServiceProps projectsInfoServiceServiceProps() { return ExternalServiceProps.builder().build(); } + @Bean("catalogProjectComponentsGroupsRestrictionConfig") + @ConfigurationProperties(prefix = "catalog.project-components.groups-restriction") + public CatalogProjectComponentsGroupsRestrictionProps catalogProjectComponentsGroupsRestrictionConfig() { + return CatalogProjectComponentsGroupsRestrictionProps.builder().build(); + } + @Bean("catalogItemGroupsRestrictionConfig") @ConfigurationProperties(prefix = "catalog.user-action.groups-restriction") public CatalogItemUserActionGroupsRestrictionProps catalogItemGroupsRestrictionConfig() { @@ -72,6 +78,12 @@ public static class BitbucketServiceCacheProps { private Duration evictionInterval; } + @Builder + @Data + public static class CatalogProjectComponentsGroupsRestrictionProps { + private List prefix; + } + @Builder @Data public static class CatalogItemUserActionGroupsRestrictionProps { diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacade.java index 9dd3a62..ce6e544 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacade.java @@ -3,7 +3,9 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_catalog.server.controllers.exceptions.ComponentNotFoundException; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.mappers.ProjectComponentExtendedInfoMapper; import org.opendevstack.component_catalog.server.mappers.ProjectComponentsInfoMapper; import org.opendevstack.component_catalog.server.model.ProjectComponentExtendedInfo; @@ -15,10 +17,7 @@ import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; import org.springframework.stereotype.Component; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; @Component @AllArgsConstructor @@ -29,6 +28,7 @@ public class ProjectComponentsFacade { private final ProjectComponentsInfoMapper projectComponentsInfoMapper; private final ProjectsInfoService projectsInfoService; private final ProjectComponentExtendedInfoMapper projectComponentExtendedInfoMapper; + private ApplicationPropertiesConfiguration.CatalogProjectComponentsGroupsRestrictionProps catalogProjectComponentsGroupsRestrictionProps; public List getProjectComponentsInfo(String projectKey, String accessToken) { var projectComponents = provisionerActionsService.getProjectComponents(projectKey); @@ -39,6 +39,10 @@ public List getProjectComponentsInfo(String projectKey, St List userGroups = projectsInfoService.getProjectGroups(accessToken); + if (!userBelongsToProjectGroups(userGroups, projectKey)) { + throw new ForbiddenException("User must belong to the project to get its components"); + } + return projectComponents.getComponents() .values() .stream() @@ -61,6 +65,11 @@ public ProjectComponentExtendedInfo getProjectComponentExtendedInfo(String proje throw new IllegalArgumentException("Valid projectKey, componentId and accessToken are mandatory."); } + List userGroups = projectsInfoService.getProjectGroups(accessToken); + if (!userBelongsToProjectGroups(userGroups, projectKey)) { + throw new ForbiddenException("User must belong to the project to get its components"); + } + return Optional.ofNullable(projectComponents.getComponents()) .orElse(Map.of()) .values() @@ -78,4 +87,12 @@ private boolean notValid(ProjectComponents projectComponents, String projectKey, projectComponents.getComponents().isEmpty() || StringUtils.isBlank(accessToken) || StringUtils.isBlank(projectKey)); } + + private boolean userBelongsToProjectGroups(List groups, String projectKey) { + if (groups == null) return false; + return groups.stream() + .filter(Objects::nonNull) + .anyMatch(g -> catalogProjectComponentsGroupsRestrictionProps.getPrefix().stream().anyMatch(g.toUpperCase()::startsWith) && + g.toUpperCase().contains(projectKey.toUpperCase())); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0b3a4a8..614aa2a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -53,6 +53,10 @@ component-catalog: base-rest-url: ${PROJECTS_INFO_SERVICE_BASE_REST_URL} catalog: + project-components: + groups-restriction: + prefix: + - BI-AS-ATLASSIAN user-action: groups-restriction: prefix: diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacadeTest.java index 2ff89ee..3893d6e 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacadeTest.java @@ -54,13 +54,18 @@ class ProjectComponentsFacadeTest { @Mock private ProjectComponentExtendedInfoMapper projectComponentExtendedInfoMapper; + @Mock + private ApplicationPropertiesConfiguration.CatalogProjectComponentsGroupsRestrictionProps catalogGroupsRestrictionProps; + @BeforeEach void setUp() { ProjectComponentsInfoMapper projectComponentsInfoMapper = new ProjectComponentsInfoMapper(catalogItemsApiFacade, catalogItemDefaultProps); - projectComponentsFacade = new ProjectComponentsFacade(provisionerActionsService, projectComponentsInfoMapper, projectsInfoService, projectComponentExtendedInfoMapper); + projectComponentsFacade = new ProjectComponentsFacade(provisionerActionsService, projectComponentsInfoMapper, + projectsInfoService, projectComponentExtendedInfoMapper, catalogGroupsRestrictionProps); lenient().when(authenticationFacade.getAccessToken()).thenReturn("accessToken"); + lenient().when(catalogGroupsRestrictionProps.getPrefix()).thenReturn(List.of("BI-AS-ATLASSIAN-P-")); } @Test @@ -79,6 +84,7 @@ void givenProjectWithTwoComponents_whenAllCatalogFetchOk_thenReturnMappedList() var pc = ProjectComponentsMother.of(comps); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(pc); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(catalogItemsApiFacade.fetchCatalogItem(any())) .thenAnswer(inv -> { var p = (CatalogRequestParams) inv.getArgument(0); @@ -128,6 +134,7 @@ void givenOneComponentFailsWithInvalidIdException_whenGetProjectComponentsInfo_t var pc = ProjectComponentsMother.of(comps); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(pc); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(catalogItemsApiFacade.fetchCatalogItem(any())) .thenAnswer(inv -> { var p = (CatalogRequestParams) inv.getArgument(0); @@ -168,6 +175,7 @@ void givenOneComponentFailsWithInvalidCatalogItemEntityException_whenGetProjectC var pc = ProjectComponentsMother.of(comps); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(pc); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(catalogItemsApiFacade.fetchCatalogItem(any())) .thenAnswer(inv -> { var p = (CatalogRequestParams) inv.getArgument(0); @@ -205,6 +213,7 @@ void givenBlankOrNullImageFileId_whenMapToProjectComponentInfo_thenLogoUrlIsEmpt var pc = ProjectComponentsMother.of(comps); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(pc); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(catalogItemsApiFacade.fetchCatalogItem(any())) .thenAnswer(inv -> { var p = (CatalogRequestParams) inv.getArgument(0); @@ -265,6 +274,7 @@ void givenExistingComponent_whenGetExtendedInfo_thenReturnMappedInfo() { var comps = ProjectComponentsMother.of(Map.of("k1", comp)); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(projectComponentExtendedInfoMapper.mapToProjectComponentExtendedInfo(comp)) .thenReturn(Optional.of(new ProjectComponentExtendedInfo())); @@ -285,6 +295,7 @@ void givenComponentDoesNotExist_whenGetExtendedInfo_thenThrowComponentNotFound() var comps = ProjectComponentsMother.of(Map.of("k1", comp)); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); // when / then assertThatThrownBy(() -> @@ -303,6 +314,7 @@ void givenMapperReturnsEmptyOptional_whenGetExtendedInfo_thenThrowComponentNotFo var comps = ProjectComponentsMother.of(Map.of("k1", comp)); when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-" + projectKey)); when(projectComponentExtendedInfoMapper.mapToProjectComponentExtendedInfo(comp)) .thenReturn(Optional.empty()); @@ -311,6 +323,167 @@ void givenMapperReturnsEmptyOptional_whenGetExtendedInfo_thenThrowComponentNotFo projectComponentsFacade.getProjectComponentExtendedInfo(projectKey, componentId, accessToken) ).isInstanceOf(ComponentNotFoundException.class); } + + + @Test + void givenNullUserGroups_whenGetProjectComponentsInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(null); + + // when / then + assertThatThrownBy(() -> projectComponentsFacade.getProjectComponentsInfo(projectKey, accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenEmptyUserGroups_whenGetProjectComponentsInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of()); + + // when / then + assertThatThrownBy(() -> projectComponentsFacade.getProjectComponentsInfo(projectKey, accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithNoMatchingPrefix_whenGetProjectComponentsInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("WRONG-PREFIX-PRJ-123")); + + // when / then + assertThatThrownBy(() -> projectComponentsFacade.getProjectComponentsInfo(projectKey, accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithMatchingPrefixButWrongProject_whenGetProjectComponentsInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-OTHER")); + + // when / then + assertThatThrownBy(() -> projectComponentsFacade.getProjectComponentsInfo(projectKey, accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithMatchingPrefixAndProject_whenGetProjectComponentsInfo_thenDoNotThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-PRJ-123")); + + // when + List result = projectComponentsFacade.getProjectComponentsInfo(projectKey, accessToken); + + // then + assertThat(result).isNotNull(); + } + + + @Test + void givenNullUserGroups_whenGetProjectComponentExtendedInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(null); + + // when / then + assertThatThrownBy(() -> + projectComponentsFacade.getProjectComponentExtendedInfo(projectKey, "C1", accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenEmptyUserGroups_whenGetProjectComponentExtendedInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of()); + + // when / then + assertThatThrownBy(() -> + projectComponentsFacade.getProjectComponentExtendedInfo(projectKey, "C1", accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithNoMatchingPrefix_whenGetProjectComponentExtendedInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("WRONG-PREFIX-PRJ-123")); + + // when / then + assertThatThrownBy(() -> + projectComponentsFacade.getProjectComponentExtendedInfo(projectKey, "C1", accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithMatchingPrefixButWrongProject_whenGetProjectComponentExtendedInfo_thenThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", + ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED)))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-OTHER")); + + // when / then + assertThatThrownBy(() -> + projectComponentsFacade.getProjectComponentExtendedInfo(projectKey, "C1", accessToken)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("User must belong to the project to get its components"); + } + + @Test + void givenGroupWithMatchingPrefixAndProject_whenGetProjectComponentExtendedInfo_thenDoNotThrowForbiddenException() { + // given + var projectKey = "PRJ-123"; + ProjectComponent comp = ProjectComponentMother.of("C1", "Y2F0LTE", "cmVmLTE", Status.CREATED); + var comps = ProjectComponentsMother.of(new LinkedHashMap<>(Map.of("k1", comp))); + when(provisionerActionsService.getProjectComponents(projectKey)).thenReturn(comps); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(List.of("BI-AS-ATLASSIAN-P-PRJ-123")); + when(projectComponentExtendedInfoMapper.mapToProjectComponentExtendedInfo(comp)) + .thenReturn(Optional.of(new ProjectComponentExtendedInfo())); + + // when + ProjectComponentExtendedInfo result = projectComponentsFacade + .getProjectComponentExtendedInfo(projectKey, "C1", accessToken); + + // then + assertThat(result).isNotNull(); + } }