From c130e6fa3b650b531c5aa3cb66ebefc80254faef Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Thu, 2 Apr 2026 09:02:30 +0200 Subject: [PATCH 1/7] Add group-based access restrictions for provisioning actions --- .../config/ControllerExceptionHandler.java | 8 ++ .../ProvisionerActionsApiController.java | 36 +++++ .../exceptions/ForbiddenException.java | 8 ++ .../ProvisionerActionsApiControllerTest.java | 135 ++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 src/main/java/org/opendevstack/component_catalog/server/controllers/exceptions/ForbiddenException.java diff --git a/src/main/java/org/opendevstack/component_catalog/config/ControllerExceptionHandler.java b/src/main/java/org/opendevstack/component_catalog/config/ControllerExceptionHandler.java index c27b6c7..1c6367c 100644 --- a/src/main/java/org/opendevstack/component_catalog/config/ControllerExceptionHandler.java +++ b/src/main/java/org/opendevstack/component_catalog/config/ControllerExceptionHandler.java @@ -2,6 +2,7 @@ import org.opendevstack.component_catalog.server.controllers.exceptions.BadConfigurationException; import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException; import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException; import org.opendevstack.component_catalog.server.model.RestErrorMessage; @@ -47,6 +48,13 @@ public ResponseEntity handleBadRequestException(BadRequestExce return defaultErrResponse(ex, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException ex) { + log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex); + + return defaultErrResponse(ex, HttpStatus.FORBIDDEN); + } + @ExceptionHandler(RestEntityNotFoundException.class) public ResponseEntity handleEntityNotFoundException(RestEntityNotFoundException ex) { log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex); diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java index 02f8b63..1232434 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java @@ -4,10 +4,18 @@ import org.apache.commons.lang3.tuple.Pair; import org.jspecify.annotations.NonNull; import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; +import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction; +import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; import org.opendevstack.component_catalog.server.services.provisioner.Status; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import com.fasterxml.jackson.core.JsonProcessingException; import lombok.AllArgsConstructor; import lombok.SneakyThrows; @@ -26,6 +34,9 @@ public class ProvisionerActionsApiController implements ProvisionerActionsApi { private final ProvisionerActionsService provisionerActionsService; + private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; + private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + private final AuthorizationInfo authorizationInfo; @SneakyThrows @Override @@ -36,6 +47,7 @@ public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); + validateGroupRestrictions(normalizedProjectKey); var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); @@ -53,6 +65,7 @@ public ResponseEntity notifyProvisioningStatusUpdatePartially(String proje projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); + validateGroupRestrictions(normalizedProjectKey); var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); @@ -81,4 +94,27 @@ public ResponseEntity deleteProvisioningStatus(String projectKey, Provisio .map(parameter -> Pair.of(parameter.getName(), parameter.getValues())) .toList(); } + + private void validateGroupRestrictions(String projectKey) { + var groupRestriction = CatalogItemUserActionGroupsRestriction.builder() + .prefix(groupsRestrictionProps.getPrefix()) + .suffix(groupsRestrictionProps.getSuffix()) + .build(); + + var userActionEntityRestrictions = UserActionEntityRestrictions.builder() + .groups(groupRestriction) + .build(); + + var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions); + + var params = RestrictionsParams.builder() + .userGroups(authorizationInfo.getCurrentRoles()) + .projectKey(projectKey) + .build(); + + if (Boolean.FALSE.equals(groupsRestrictionsEvaluator.evaluate(evaluationRestrictions, params).getLeft())) { + log.error("The user has no permissions to perform this action based on group restrictions for project {}", projectKey); + throw new ForbiddenException("User not allowed to perform this action"); + } + } } diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/exceptions/ForbiddenException.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/exceptions/ForbiddenException.java new file mode 100644 index 0000000..d8e952d --- /dev/null +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/exceptions/ForbiddenException.java @@ -0,0 +1,8 @@ +package org.opendevstack.component_catalog.server.controllers.exceptions; + +public class ForbiddenException extends RuntimeException { + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java index e7e4eb9..a55f78a 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java @@ -6,16 +6,25 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; +import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) class ProvisionerActionsApiControllerTest { @@ -23,6 +32,15 @@ class ProvisionerActionsApiControllerTest { @Mock private ProvisionerActionsService provisionerActionsService; + @Mock + private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; + + @Mock + private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + + @Mock + private AuthorizationInfo authorizationInfo; + @InjectMocks private ProvisionerActionsApiController provisionerActionsApiController; @@ -46,6 +64,9 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro .componentUrl(componentUrl) .parameters(parameters); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "")); + // when provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); @@ -74,6 +95,9 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC .componentUrl(componentUrl) .parameters(parameters); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "")); + // when provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); @@ -88,6 +112,64 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC ); } + @Test + void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsCalledWithEmptyUrl() throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var status = Status.CREATING; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parameters = List.of(parameter); + + var request = new ProvisioningStatusUpdateRequest() + .componentId(componentId) + .catalogItemId(catalogItemId) + .parameters(parameters); + + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "")); + + // when + provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); + + // then + verify(provisionerActionsService).updatePartiallyComponentProvisioningStatus(projectKey.toUpperCase(), + status, componentId, catalogItemId, "", List.of(Pair.of(parameter.getName(), parameter.getValues()))); + } + + @Test + void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdate_thenServiceIsCalledWithEmptyUrl() throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var status = Status.CREATED; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parameters = List.of(parameter); + + var request = new ProvisioningStatusUpdateRequest() + .componentId(componentId) + .catalogItemId(catalogItemId) + .parameters(parameters); + + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "")); + + // when + provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); + + // then + verify(provisionerActionsService).updateComponentProvisioningStatus(projectKey.toUpperCase(), + status, componentId, catalogItemId, "", List.of(Pair.of(parameter.getName(), parameter.getValues()))); + } + @Test void givenAProjectKey_whenDeleteProvisioningStatus_thenServiceIsCalled() throws JsonProcessingException { // given @@ -103,4 +185,57 @@ void givenAProjectKey_whenDeleteProvisioningStatus_thenServiceIsCalled() throws // then verify(provisionerActionsService).deleteComponentProvisioningStatus(projectKey, componentId); } + + @Test + void givenAProjectKey_whenDeleteProvisioningStatusThrowsException_thenUnprocessableEntityIsReturned() throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var componentId = "componentId"; + + var request = new ProvisioningDeleteRequest() + .componentId(componentId); + + org.mockito.Mockito.doThrow(new JsonProcessingException("Error") {}) + .when(provisionerActionsService).deleteComponentProvisioningStatus(projectKey, componentId); + + // when + var response = provisionerActionsApiController.deleteProvisioningStatus(projectKey, request); + + // then + org.junit.jupiter.api.Assertions.assertEquals(org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + } + + @Test + void givenUserWithoutPermissions_whenNotifyProvisioningStatusUpdate_thenForbiddenExceptionIsThrown() { + // given + var projectKey = "projectKey"; + var request = new ProvisioningStatusUpdateRequest() + .componentId("id") + .catalogItemId("catalogId") + .parameters(List.of()); + + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(false, "Forbidden")); + + // when / then + assertThrows(ForbiddenException.class, () -> + provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, Status.CREATED.name(), request)); + } + + @Test + void givenUserWithoutPermissions_whenNotifyProvisioningStatusUpdatePartially_thenForbiddenExceptionIsThrown() { + // given + var projectKey = "projectKey"; + var request = new ProvisioningStatusUpdateRequest() + .componentId("id") + .catalogItemId("catalogId") + .parameters(List.of()); + + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(false, "Forbidden")); + + // when / then + assertThrows(ForbiddenException.class, () -> + provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, Status.CREATED.name(), request)); + } } From 20f092a4c4807fd625c9fc8a2de8c84de8651b7d Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 12:21:26 +0200 Subject: [PATCH 2/7] Introduce `ProvisionerActionsApiFacade` to streamline group-based validation in provisioning actions and reduce controller complexity. --- openapi/openapi-component_catalog-v1.0.0.yaml | 6 + .../config/SecurityConfiguration.java | 1 - .../ProvisionerActionsApiController.java | 66 ++----- .../facade/ProvisionerActionsApiFacade.java | 78 +++++++++ .../ProvisionerActionsApiControllerTest.java | 78 ++------- .../ProvisionerActionsApiFacadeTest.java | 164 ++++++++++++++++++ 6 files changed, 277 insertions(+), 116 deletions(-) create mode 100644 src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java create mode 100644 src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java diff --git a/openapi/openapi-component_catalog-v1.0.0.yaml b/openapi/openapi-component_catalog-v1.0.0.yaml index 571a9c1..d89f895 100644 --- a/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/openapi/openapi-component_catalog-v1.0.0.yaml @@ -1151,6 +1151,12 @@ components: example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog" nullable: true + accessToken: + type: string + description: the access token to be used to get azure groups + example: "some-access-token" + nullable: false + parameters: type: array description: List of name/value string parameters. diff --git a/src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java b/src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java index 9f6c77e..9d8b6e5 100644 --- a/src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java +++ b/src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java @@ -80,7 +80,6 @@ public SecurityFilterChain aadForEverythingElse(HttpSecurity http) throws Except PathPatternRequestMatcher.withDefaults().matcher("/swagger-ui/**"), PathPatternRequestMatcher.withDefaults().matcher("/v3/api-docs/**"), PathPatternRequestMatcher.withDefaults().matcher("/v1/user-actions/**"), - PathPatternRequestMatcher.withDefaults().matcher("/v1/provision/*/*"), PathPatternRequestMatcher.withDefaults().matcher("/actuator/health") ); diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java index 1232434..53a3a89 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java @@ -1,31 +1,21 @@ package org.opendevstack.component_catalog.server.controllers; -import jakarta.validation.constraints.NotNull; -import org.apache.commons.lang3.tuple.Pair; -import org.jspecify.annotations.NonNull; -import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi; -import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; -import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; -import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; -import org.opendevstack.component_catalog.server.security.AuthorizationInfo; -import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; -import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction; -import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; -import org.opendevstack.component_catalog.server.services.provisioner.Status; -import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import com.fasterxml.jackson.core.JsonProcessingException; import lombok.AllArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi; +import org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade; +import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; +import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; +import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import java.util.List; +import static org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade.map; @Controller @RequestMapping("${openapi.componentCatalogREST.base-path:/v1}") @@ -33,10 +23,9 @@ @Slf4j public class ProvisionerActionsApiController implements ProvisionerActionsApi { + private final ProvisionerActionsApiFacade provisionerActionsApiFacade; private final ProvisionerActionsService provisionerActionsService; - private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; - private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; - private final AuthorizationInfo authorizationInfo; + @SneakyThrows @Override @@ -47,13 +36,13 @@ public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); - validateGroupRestrictions(normalizedProjectKey); + provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest); var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); provisionerActionsService.updateComponentProvisioningStatus(normalizedProjectKey, Status.valueOf(status), - provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(), - normalizedComponentUrl, parameters); + provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(), + normalizedComponentUrl, parameters); return ResponseEntity.ok().build(); } @@ -65,7 +54,7 @@ public ResponseEntity notifyProvisioningStatusUpdatePartially(String proje projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); - validateGroupRestrictions(normalizedProjectKey); + provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest); var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); @@ -88,33 +77,4 @@ public ResponseEntity deleteProvisioningStatus(String projectKey, Provisio return ResponseEntity.ok().build(); } - - private static @NonNull List>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { - return provisioningStatusUpdateRequest.getParameters().stream() - .map(parameter -> Pair.of(parameter.getName(), parameter.getValues())) - .toList(); - } - - private void validateGroupRestrictions(String projectKey) { - var groupRestriction = CatalogItemUserActionGroupsRestriction.builder() - .prefix(groupsRestrictionProps.getPrefix()) - .suffix(groupsRestrictionProps.getSuffix()) - .build(); - - var userActionEntityRestrictions = UserActionEntityRestrictions.builder() - .groups(groupRestriction) - .build(); - - var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions); - - var params = RestrictionsParams.builder() - .userGroups(authorizationInfo.getCurrentRoles()) - .projectKey(projectKey) - .build(); - - if (Boolean.FALSE.equals(groupsRestrictionsEvaluator.evaluate(evaluationRestrictions, params).getLeft())) { - log.error("The user has no permissions to perform this action based on group restrictions for project {}", projectKey); - throw new ForbiddenException("User not allowed to perform this action"); - } - } } diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java new file mode 100644 index 0000000..da7485d --- /dev/null +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -0,0 +1,78 @@ +package org.opendevstack.component_catalog.server.facade; + +import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.jspecify.annotations.NonNull; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; +import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_catalog.server.services.ProjectsInfoService; +import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction; +import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.List; + +@Component +@AllArgsConstructor +@Slf4j +public class ProvisionerActionsApiFacade { + private final ProjectsInfoService projectsInfoService; + private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; + private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + + public static @NonNull List>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + return provisioningStatusUpdateRequest.getParameters().stream() + .map(parameter -> Pair.of(parameter.getName(), parameter.getValues())) + .toList(); + } + + public void validateGroupRestrictions(String projectKey, ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + var groupRestriction = CatalogItemUserActionGroupsRestriction.builder() + .prefix(groupsRestrictionProps.getPrefix()) + .suffix(groupsRestrictionProps.getSuffix()) + .build(); + + var userActionEntityRestrictions = UserActionEntityRestrictions.builder() + .groups(groupRestriction) + .build(); + + var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions); + var userGroups = projectsInfoService.getProjectGroups(getIdToken(), provisioningStatusUpdateRequest.getAccessToken()); + + var params = RestrictionsParams.builder() + .userGroups(userGroups) + .projectKey(projectKey) + .build(); + + if (Boolean.FALSE.equals(groupsRestrictionsEvaluator.evaluate(evaluationRestrictions, params).getLeft())) { + log.error("The user has no permissions to perform this action based on group restrictions for project {}", projectKey); + throw new ForbiddenException("User not allowed to perform this action"); + } + } + + public String getIdToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + log.debug("Authenticated user '{}'", auth.getName()); + + var principal = (UserPrincipal) auth.getPrincipal(); + + var idToken = principal.getAadIssuedBearerToken(); + + log.debug("Extracted idToken: {} from request.", idToken); + + return idToken; + } + +} diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java index a55f78a..aaba026 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java @@ -6,25 +6,19 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; -import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; +import org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade; import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; -import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; -import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; @ExtendWith(SpringExtension.class) class ProvisionerActionsApiControllerTest { @@ -33,13 +27,7 @@ class ProvisionerActionsApiControllerTest { private ProvisionerActionsService provisionerActionsService; @Mock - private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; - - @Mock - private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; - - @Mock - private AuthorizationInfo authorizationInfo; + private ProvisionerActionsApiFacade provisionerActionsApiFacade; @InjectMocks private ProvisionerActionsApiController provisionerActionsApiController; @@ -64,15 +52,15 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro .componentUrl(componentUrl) .parameters(parameters); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(true, "")); + var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); // when provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); // then + verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase()), eq(request)); verify(provisionerActionsService).updateComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, componentUrl, List.of(Pair.of(parameter.getName(), parameter.getValues()))); + status, componentId, catalogItemId, componentUrl, mappedParameters); } @Test @@ -95,20 +83,20 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC .componentUrl(componentUrl) .parameters(parameters); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(true, "")); + var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); // when provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); // then + verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase()), eq(request)); verify(provisionerActionsService).updatePartiallyComponentProvisioningStatus( projectKey.toUpperCase(), status, componentId, catalogItemId, componentUrl, - List.of(Pair.of(parameter.getName(), parameter.getValues())) + mappedParameters ); } @@ -130,15 +118,15 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdatePartial .catalogItemId(catalogItemId) .parameters(parameters); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(true, "")); + var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); // when provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); // then + verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase()), eq(request)); verify(provisionerActionsService).updatePartiallyComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, "", List.of(Pair.of(parameter.getName(), parameter.getValues()))); + status, componentId, catalogItemId, "", mappedParameters); } @Test @@ -159,15 +147,15 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdate_thenSe .catalogItemId(catalogItemId) .parameters(parameters); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(true, "")); + var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); // when provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); // then + verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase()), eq(request)); verify(provisionerActionsService).updateComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, "", List.of(Pair.of(parameter.getName(), parameter.getValues()))); + status, componentId, catalogItemId, "", mappedParameters); } @Test @@ -204,38 +192,4 @@ void givenAProjectKey_whenDeleteProvisioningStatusThrowsException_thenUnprocessa // then org.junit.jupiter.api.Assertions.assertEquals(org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); } - - @Test - void givenUserWithoutPermissions_whenNotifyProvisioningStatusUpdate_thenForbiddenExceptionIsThrown() { - // given - var projectKey = "projectKey"; - var request = new ProvisioningStatusUpdateRequest() - .componentId("id") - .catalogItemId("catalogId") - .parameters(List.of()); - - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(false, "Forbidden")); - - // when / then - assertThrows(ForbiddenException.class, () -> - provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, Status.CREATED.name(), request)); - } - - @Test - void givenUserWithoutPermissions_whenNotifyProvisioningStatusUpdatePartially_thenForbiddenExceptionIsThrown() { - // given - var projectKey = "projectKey"; - var request = new ProvisioningStatusUpdateRequest() - .componentId("id") - .catalogItemId("catalogId") - .parameters(List.of()); - - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(false, "Forbidden")); - - // when / then - assertThrows(ForbiddenException.class, () -> - provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, Status.CREATED.name(), request)); - } } diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java new file mode 100644 index 0000000..6be3169 --- /dev/null +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -0,0 +1,164 @@ +package org.opendevstack.component_catalog.server.facade; + +import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; +import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; +import org.opendevstack.component_catalog.server.security.AuthorizationInfo; +import org.opendevstack.component_catalog.server.services.ProjectsInfoService; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; +import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ProvisionerActionsApiFacadeTest { + + @Mock + private ProjectsInfoService projectsInfoService; + @Mock + private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; + @Mock + private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + @Mock + private AuthorizationInfo authorizationInfo; + + @InjectMocks + private ProvisionerActionsApiFacade provisionerActionsApiFacade; + + @BeforeEach + void setUp() { + } + + @Test + void map_convertsParametersToPairs() { + // given + var parameter1 = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("param1") + .values(List.of("value1", "value2")) + .build(); + var parameter2 = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("param2") + .values(List.of("value3")) + .build(); + var request = new ProvisioningStatusUpdateRequest() + .parameters(List.of(parameter1, parameter2)); + + // when + var result = ProvisionerActionsApiFacade.map(request); + + // then + assertThat(result).containsExactly( + Pair.of("param1", List.of("value1", "value2")), + Pair.of("param2", List.of("value3")) + ); + } + + @Test + void map_withEmptyParameters_returnsEmptyList() { + // given + var request = new ProvisioningStatusUpdateRequest().parameters(List.of()); + + // when + var result = ProvisionerActionsApiFacade.map(request); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validateGroupRestrictions_whenUserHasPermissions_doesNotThrow() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var idToken = "idToken"; + var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); + var userGroups = List.of("group1"); + + mockSecurityContext(idToken); + + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "Allowed")); + + // when / then + provisionerActionsApiFacade.validateGroupRestrictions(projectKey, request); + } + + @Test + void validateGroupRestrictions_whenUserHasNoPermissions_throwsForbiddenException() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var idToken = "idToken"; + var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); + var userGroups = List.of("group1"); + + mockSecurityContext(idToken); + + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(false, "Forbidden")); + + // when / then + assertThatThrownBy(() -> provisionerActionsApiFacade.validateGroupRestrictions(projectKey, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not allowed to perform this action"); + } + + private void mockSecurityContext(String idToken) { + var authentication = mock(Authentication.class); + var securityContext = mock(SecurityContext.class); + var principal = mock(UserPrincipal.class); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(principal); + when(principal.getAadIssuedBearerToken()).thenReturn(idToken); + when(authentication.getName()).thenReturn("userName"); + + SecurityContextHolder.setContext(securityContext); + } + + @Test + void validateGroupRestrictions_whenEvaluatorReturnsNull_doesNotThrow() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var idToken = "idToken"; + var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); + var userGroups = List.of("group1"); + + mockSecurityContext(idToken); + + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(null, "Unknown")); + + // when / then + provisionerActionsApiFacade.validateGroupRestrictions(projectKey, request); + } +} From 93dc0fcca67217e2b79e0cf85a2c7daf5ef65d47 Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 12:22:18 +0200 Subject: [PATCH 3/7] Remove unused imports and minor formatting adjustments in `ProvisionerActionsApiFacade` and related test --- .../server/facade/ProvisionerActionsApiFacade.java | 2 -- .../controllers/ProvisionerActionsApiControllerTest.java | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java index da7485d..22e6519 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -18,8 +18,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import java.util.List; diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java index aaba026..6232a13 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java @@ -16,9 +16,8 @@ import java.util.List; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; @ExtendWith(SpringExtension.class) class ProvisionerActionsApiControllerTest { @@ -183,7 +182,8 @@ void givenAProjectKey_whenDeleteProvisioningStatusThrowsException_thenUnprocessa var request = new ProvisioningDeleteRequest() .componentId(componentId); - org.mockito.Mockito.doThrow(new JsonProcessingException("Error") {}) + org.mockito.Mockito.doThrow(new JsonProcessingException("Error") { + }) .when(provisionerActionsService).deleteComponentProvisioningStatus(projectKey, componentId); // when From 02a135783da64686e71245e0677d8b83a27e11b8 Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 12:29:34 +0200 Subject: [PATCH 4/7] Remove unused imports and `setUp` method from `ProvisionerActionsApiFacadeTest` --- .../server/facade/ProvisionerActionsApiFacadeTest.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java index 6be3169..e3d353b 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -2,7 +2,6 @@ import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; 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.InjectMocks; @@ -12,7 +11,6 @@ import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; -import org.opendevstack.component_catalog.server.security.AuthorizationInfo; import org.opendevstack.component_catalog.server.services.ProjectsInfoService; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; @@ -38,15 +36,9 @@ class ProvisionerActionsApiFacadeTest { private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; @Mock private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; - @Mock - private AuthorizationInfo authorizationInfo; - @InjectMocks - private ProvisionerActionsApiFacade provisionerActionsApiFacade; - @BeforeEach - void setUp() { - } + private ProvisionerActionsApiFacade provisionerActionsApiFacade; @Test void map_convertsParametersToPairs() { From f15e67b849fbf0a702f65118953c859006e4feae Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 15:07:09 +0200 Subject: [PATCH 5/7] Replace direct authentication token retrieval with `AuthenticationFacade` to centralize authentication logic and simplify security handling across facades and tests. --- .../CatalogItemsApiController.java | 10 +++-- .../server/facade/CatalogItemsApiFacade.java | 17 +------ .../facade/ProjectComponentsFacade.java | 24 +++------- .../facade/ProvisionerActionsApiFacade.java | 22 ++------- .../CatalogItemsApiControllerTest.java | 10 ++--- .../facade/CatalogItemsApiFacadeTest.java | 29 +++++++++--- .../facade/ProjectComponentsFacadeTest.java | 45 ++++++++++--------- .../ProvisionerActionsApiFacadeTest.java | 30 +++---------- 8 files changed, 73 insertions(+), 114 deletions(-) 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 ff2a82c..502f141 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 @@ -1,9 +1,12 @@ package org.opendevstack.component_catalog.server.controllers; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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; import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException; +import org.opendevstack.component_catalog.server.facade.AuthenticationFacade; import org.opendevstack.component_catalog.server.facade.CatalogItemsApiFacade; import org.opendevstack.component_catalog.server.model.CatalogItem; import org.opendevstack.component_catalog.server.model.SortOrder; @@ -11,8 +14,6 @@ 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.exceptions.InvalidIdException; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,6 +28,7 @@ public class CatalogItemsApiController implements CatalogItemsApi { private final AuthorizationInfo authInfo; private final CatalogItemsApiFacade catalogItemsApiFacade; + private final AuthenticationFacade authenticationFacade; @Override public ResponseEntity> getCatalogItems(String catalogId, SortOrder sortByTitle) { @@ -51,7 +53,7 @@ public ResponseEntity> getCatalogItemsForProjectKey(String cat log.debug("User '{}' requested catalog items for catalog id and projectKey: '{}', '{}'", authInfo.getCurrentPrincipalName(), catalogId, projectKey); try { - var idToken = catalogItemsApiFacade.getIdToken(); + var idToken = authenticationFacade.getIdToken(); var catalogItemRequestParams = CatalogRequestParams.builder() .catalogId(catalogId) @@ -93,7 +95,7 @@ public ResponseEntity getCatalogItemByIdForProjectKey(String id, St log.debug("User '{}' requested catalog item with id and projectKey: '{}', '{}'", authInfo.getCurrentPrincipalName(), id, projectKey); try { - var idToken = catalogItemsApiFacade.getIdToken(); + var idToken = authenticationFacade.getIdToken(); var catalogRequestParams = CatalogRequestParams.builder() .catalogItemId(id) 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 803281c..9395361 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 @@ -1,6 +1,7 @@ package org.opendevstack.component_catalog.server.facade; -import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.opendevstack.component_catalog.client.projects_info_service.v1_0_0.model.ProjectInfo; import org.opendevstack.component_catalog.server.controllers.CatalogApiAdapter; import org.opendevstack.component_catalog.server.controllers.CatalogRequestParams; @@ -16,10 +17,6 @@ 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.exceptions.InvalidIdException; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.Collections; @@ -54,16 +51,6 @@ public List catalogItemFiltersFrom(CatalogRequestParams catal return catalogApiAdapter.catalogItemFiltersFrom(catalogRequestParams, clusters, userGroups); } - public String getIdToken() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - log.debug("Authenticated user '{}'", auth.getName()); - - var principal = (UserPrincipal) auth.getPrincipal(); - - return principal.getAadIssuedBearerToken(); - } - private List getProjectGroups(CatalogRequestParams catalogRequestParams) { if (catalogRequestParams.getAccessToken() == null) { return Collections.emptyList(); 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 8dde666..04b0336 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 @@ -1,6 +1,8 @@ package org.opendevstack.component_catalog.server.facade; -import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.opendevstack.component_catalog.server.mappers.ProjectComponentsInfoMapper; import org.opendevstack.component_catalog.server.model.ProjectComponentInfo; import org.opendevstack.component_catalog.server.services.ProjectsInfoService; @@ -8,11 +10,6 @@ import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException; import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException; import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.Collections; @@ -27,16 +24,7 @@ public class ProjectComponentsFacade { private final ProvisionerActionsService provisionerActionsService; private final ProjectComponentsInfoMapper projectComponentsInfoMapper; private final ProjectsInfoService projectsInfoService; - - public String getIdToken() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - log.debug("Authenticated user '{}'", auth.getName()); - - var principal = (UserPrincipal) auth.getPrincipal(); - - return principal.getAadIssuedBearerToken(); - } + private final AuthenticationFacade authenticationFacade; public List getProjectComponentsInfo(String projectKey, String accessToken) { var projectComponents = provisionerActionsService.getProjectComponents(projectKey); @@ -45,8 +33,8 @@ public List getProjectComponentsInfo(String projectKey, St return Collections.emptyList(); } - String idToken = getIdToken(); - List userGroups = projectsInfoService.getProjectGroups(idToken,accessToken); + String idToken = authenticationFacade.getIdToken(); + List userGroups = projectsInfoService.getProjectGroups(idToken, accessToken); return projectComponents.getComponents() .values() diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java index 22e6519..52dab48 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -1,6 +1,5 @@ package org.opendevstack.component_catalog.server.facade; -import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,8 +14,6 @@ import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.List; @@ -28,6 +25,7 @@ public class ProvisionerActionsApiFacade { private final ProjectsInfoService projectsInfoService; private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + private final AuthenticationFacade authenticationFacade; public static @NonNull List>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { return provisioningStatusUpdateRequest.getParameters().stream() @@ -46,7 +44,7 @@ public void validateGroupRestrictions(String projectKey, ProvisioningStatusUpdat .build(); var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions); - var userGroups = projectsInfoService.getProjectGroups(getIdToken(), provisioningStatusUpdateRequest.getAccessToken()); + var userGroups = projectsInfoService.getProjectGroups(authenticationFacade.getIdToken(), provisioningStatusUpdateRequest.getAccessToken()); var params = RestrictionsParams.builder() .userGroups(userGroups) @@ -59,18 +57,4 @@ public void validateGroupRestrictions(String projectKey, ProvisioningStatusUpdat } } - public String getIdToken() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - - log.debug("Authenticated user '{}'", auth.getName()); - - var principal = (UserPrincipal) auth.getPrincipal(); - - var idToken = principal.getAadIssuedBearerToken(); - - log.debug("Extracted idToken: {} from request.", idToken); - - return idToken; - } - -} +} \ No newline at end of file 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 0a9bd8d..686fd88 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,5 +1,10 @@ package org.opendevstack.component_catalog.server.controllers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; 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; @@ -10,11 +15,6 @@ 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.exceptions.InvalidIdException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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 ef191bc..943a9d6 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 @@ -1,9 +1,17 @@ package org.opendevstack.component_catalog.server.facade; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.client.projects_info_service.v1_0_0.model.ProjectInfo; import org.opendevstack.component_catalog.server.controllers.CatalogApiAdapter; import org.opendevstack.component_catalog.server.controllers.CatalogRequestParams; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.model.CatalogItem; import org.opendevstack.component_catalog.server.model.CatalogItemFilter; import org.opendevstack.component_catalog.server.model.SortOrder; @@ -17,13 +25,6 @@ import org.opendevstack.component_catalog.server.services.catalog.business.UserActionsEntity; import org.opendevstack.component_catalog.server.services.catalog.entity.CatalogItemEntityContext; import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException; -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; import java.util.Collections; import java.util.List; @@ -49,6 +50,9 @@ class CatalogItemsApiFacadeTest { @Mock private UserActionsEntitiesService userActionsEntitiesService; + @Mock + private AuthenticationFacade authenticationFacade; + @Spy @InjectMocks private CatalogItemsApiFacade catalogItemsApiFacade; @@ -510,4 +514,15 @@ void fetchCatalogItem_whenAsCatalogItemThrows_propagatesInvalidCatalogItemEntity .hasMessageContaining("invalid item"); } + @Test + void getIdToken_whenAuthIsNull_throwsForbiddenException() { + // given + when(authenticationFacade.getIdToken()).thenThrow(new ForbiddenException("User not authenticated")); + + // when / then + assertThatThrownBy(() -> authenticationFacade.getIdToken()) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not authenticated"); + } + } 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 4e64d8b..ab0e8ea 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 @@ -1,8 +1,14 @@ package org.opendevstack.component_catalog.server.facade; -import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_catalog.server.controllers.CatalogRequestParams; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; import org.opendevstack.component_catalog.server.mappers.CatalogItemMother; import org.opendevstack.component_catalog.server.mappers.ProjectComponentMother; import org.opendevstack.component_catalog.server.mappers.ProjectComponentsInfoMapper; @@ -13,22 +19,12 @@ import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException; import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponent; import org.opendevstack.component_catalog.server.services.provisioner.Status; -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.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextImpl; import java.util.LinkedHashMap; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; @@ -47,6 +43,9 @@ class ProjectComponentsFacadeTest { @Mock private ProjectsInfoService projectsInfoService; + @Mock + private AuthenticationFacade authenticationFacade; + @InjectMocks private ProjectComponentsFacade projectComponentsFacade; @@ -54,16 +53,9 @@ class ProjectComponentsFacadeTest { void setUp() { ProjectComponentsInfoMapper projectComponentsInfoMapper = new ProjectComponentsInfoMapper(catalogItemsApiFacade, catalogItemDefaultProps); - projectComponentsFacade = new ProjectComponentsFacade(provisionerActionsService, projectComponentsInfoMapper, projectsInfoService); - - Authentication auth = new UsernamePasswordAuthenticationToken( - new UserPrincipal("idToken", null, null), // principal (can be UserDetails) - "password", // ignored - List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) - ); + projectComponentsFacade = new ProjectComponentsFacade(provisionerActionsService, projectComponentsInfoMapper, projectsInfoService, authenticationFacade); - // Put it into the SecurityContext - SecurityContextHolder.setContext(new SecurityContextImpl(auth)); + lenient().when(authenticationFacade.getIdToken()).thenReturn("idToken"); } @Test @@ -246,6 +238,17 @@ void givenNoComponents_whenGetProjectComponentsInfo_thenReturnEmptyList() throws assertThat(result).isEmpty(); verify(catalogItemsApiFacade, times(0)).fetchCatalogItem(any()); } + + @Test + void getIdToken_whenAuthIsNull_throwsForbiddenException() { + // given + when(authenticationFacade.getIdToken()).thenThrow(new ForbiddenException("User not authenticated")); + + // when / then + assertThatThrownBy(() -> authenticationFacade.getIdToken()) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not authenticated"); + } } diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java index e3d353b..3cfba44 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -1,6 +1,5 @@ package org.opendevstack.component_catalog.server.facade; -import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,16 +14,12 @@ import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -36,8 +31,9 @@ class ProvisionerActionsApiFacadeTest { private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; @Mock private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + @Mock + private AuthenticationFacade authenticationFacade; @InjectMocks - private ProvisionerActionsApiFacade provisionerActionsApiFacade; @Test @@ -85,8 +81,7 @@ void validateGroupRestrictions_whenUserHasPermissions_doesNotThrow() { var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); var userGroups = List.of("group1"); - mockSecurityContext(idToken); - + when(authenticationFacade.getIdToken()).thenReturn(idToken); when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); @@ -106,8 +101,7 @@ void validateGroupRestrictions_whenUserHasNoPermissions_throwsForbiddenException var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); var userGroups = List.of("group1"); - mockSecurityContext(idToken); - + when(authenticationFacade.getIdToken()).thenReturn(idToken); when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); @@ -120,19 +114,6 @@ void validateGroupRestrictions_whenUserHasNoPermissions_throwsForbiddenException .hasMessage("User not allowed to perform this action"); } - private void mockSecurityContext(String idToken) { - var authentication = mock(Authentication.class); - var securityContext = mock(SecurityContext.class); - var principal = mock(UserPrincipal.class); - - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(principal); - when(principal.getAadIssuedBearerToken()).thenReturn(idToken); - when(authentication.getName()).thenReturn("userName"); - - SecurityContextHolder.setContext(securityContext); - } - @Test void validateGroupRestrictions_whenEvaluatorReturnsNull_doesNotThrow() { // given @@ -142,8 +123,7 @@ void validateGroupRestrictions_whenEvaluatorReturnsNull_doesNotThrow() { var request = new ProvisioningStatusUpdateRequest().accessToken(accessToken); var userGroups = List.of("group1"); - mockSecurityContext(idToken); - + when(authenticationFacade.getIdToken()).thenReturn(idToken); when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); when(projectsInfoService.getProjectGroups(idToken, accessToken)).thenReturn(userGroups); From 9ed0afa54817b0e83ee9cd4b1fc68bd34769c0f4 Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 15:08:22 +0200 Subject: [PATCH 6/7] Introduce `AuthenticationFacade` to centralize authentication logic and add corresponding unit tests --- .../server/facade/AuthenticationFacade.java | 25 +++++++ .../facade/AuthenticationFacadeTest.java | 74 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/main/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacade.java create mode 100644 src/test/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacadeTest.java diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacade.java new file mode 100644 index 0000000..45ea4c1 --- /dev/null +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacade.java @@ -0,0 +1,25 @@ +package org.opendevstack.component_catalog.server.facade; + +import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AuthenticationFacade { + + public String getIdToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || !(auth.getPrincipal() instanceof UserPrincipal principal)) { + throw new ForbiddenException("User not authenticated"); + } + + log.debug("Authenticated user '{}'", auth.getName()); + + return principal.getAadIssuedBearerToken(); + } +} diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacadeTest.java new file mode 100644 index 0000000..09c5a79 --- /dev/null +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/AuthenticationFacadeTest.java @@ -0,0 +1,74 @@ +package org.opendevstack.component_catalog.server.facade; + +import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthenticationFacadeTest { + + private final AuthenticationFacade authenticationFacade = new AuthenticationFacade(); + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void getIdToken_whenAuthIsNull_throwsForbiddenException() { + // given + var securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(null); + SecurityContextHolder.setContext(securityContext); + + // when / then + assertThatThrownBy(authenticationFacade::getIdToken) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not authenticated"); + } + + @Test + void getIdToken_whenPrincipalIsNotUserPrincipal_throwsForbiddenException() { + // given + var authentication = mock(Authentication.class); + var securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn("not-a-user-principal"); + SecurityContextHolder.setContext(securityContext); + + // when / then + assertThatThrownBy(authenticationFacade::getIdToken) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not authenticated"); + } + + @Test + void getIdToken_whenAuthenticated_returnsToken() { + // given + var idToken = "token"; + var authentication = mock(Authentication.class); + var securityContext = mock(SecurityContext.class); + var principal = mock(UserPrincipal.class); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(principal); + when(principal.getAadIssuedBearerToken()).thenReturn(idToken); + when(authentication.getName()).thenReturn("userName"); + + SecurityContextHolder.setContext(securityContext); + + // when + var result = authenticationFacade.getIdToken(); + + // then + assertThat(result).isEqualTo(idToken); + } +} From 59e50a314964dbfbcad78dad590a946652556ba8 Mon Sep 17 00:00:00 2001 From: "zxBCN Esperalta_Gata,Sergio (IT EDP) EXTERNAL" Date: Tue, 7 Apr 2026 15:22:17 +0200 Subject: [PATCH 7/7] Add `AuthenticationFacade` usage in `CatalogItemsApiControllerTest` for handling authentication logic --- .../controllers/CatalogItemsApiControllerTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 686fd88..8ace7cd 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 @@ -8,6 +8,7 @@ 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; +import org.opendevstack.component_catalog.server.facade.AuthenticationFacade; import org.opendevstack.component_catalog.server.facade.CatalogItemsApiFacade; import org.opendevstack.component_catalog.server.model.CatalogItem; import org.opendevstack.component_catalog.server.model.SortOrder; @@ -43,6 +44,9 @@ class CatalogItemsApiControllerTest { @Mock private CatalogItemsApiFacade catalogItemsApiFacade; + @Mock + private AuthenticationFacade authenticationFacade; + @InjectMocks private CatalogItemsApiController catalogItemsApiController; @@ -53,6 +57,7 @@ void givenValidCatalogId_WhenGetCatalogItems_ThenReturnItemsList() throws Invali item.setId("item-1"); item.setTitle("Item 1"); + when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); when(catalogItemsApiFacade.fetchCatalogItems(any())).thenReturn(List.of(item)); // When @@ -95,6 +100,8 @@ void givenValidProjectKey_WhenGetCatalogItemsForProjectKey_ThenReturnItemsList() item.setId("item-1"); item.setTitle("Item 1"); + when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); when(catalogItemsApiFacade.fetchCatalogItems(any())).thenReturn(List.of(item)); // When @@ -110,6 +117,7 @@ void givenValidProjectKey_WhenGetCatalogItemsForProjectKey_ThenReturnItemsList() @Test void givenInvalidProjectKey_WhenGetCatalogItemsForProjectKey_ThenThrowBadRequestException() throws InvalidIdException { when(authInfo.getCurrentPrincipalName()).thenReturn("testUser"); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); when(catalogItemsApiFacade.fetchCatalogItems(any())).thenThrow(new InvalidIdException("Invalid ID")); // When / Then @@ -121,6 +129,7 @@ void givenInvalidProjectKey_WhenGetCatalogItemsForProjectKey_ThenThrowBadRequest @Test void givenEmptyResult_WhenGetCatalogItemsForProjectKey_ThenReturnEmptyList() throws InvalidCatalogEntityException { when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); // When var response = catalogItemsApiController.getCatalogItemsForProjectKey(catalogId, token, SortOrder.ASC, projectKey); @@ -193,6 +202,7 @@ void givenCatalogItemNotFound_WhenGetCatalogItemById_ThenReturnNotFound() throws @Test void givenValidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenReturnItem() throws InvalidIdException, InvalidCatalogItemEntityException { when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); CatalogItem catalogItem = new CatalogItem(); catalogItem.setId(catalogItemId); when(catalogItemsApiFacade.fetchCatalogItem(any())).thenReturn(catalogItem); @@ -208,6 +218,7 @@ void givenValidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenReturnItem() th @Test void givenInvalidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenThrowRestEntityNotFoundException() throws InvalidIdException { when(authInfo.getCurrentPrincipalName()).thenReturn("testUser"); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); when(catalogItemsApiFacade.fetchCatalogItem(any())).thenThrow(new InvalidIdException("Invalid ID")); // When / Then @@ -220,6 +231,7 @@ void givenInvalidCatalogId_WhenGetCatalogItemByIdForProjectKey_ThenThrowRestEnti void givenInvalidCatalogItemEntity_WhenGetCatalogItemByIdForProjectKey_ThenThrowInvalidRestEntityException() throws InvalidCatalogItemEntityException, InvalidIdException { when(authInfo.getCurrentPrincipalName()).thenReturn("testUser"); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); when(catalogItemsApiFacade.fetchCatalogItem(any())).thenThrow(new InvalidCatalogItemEntityException("Invalid ID")); @@ -232,6 +244,7 @@ void givenInvalidCatalogItemEntity_WhenGetCatalogItemByIdForProjectKey_ThenThrow @Test void givenCatalogItemNotFound_WhenGetCatalogItemByIdForProjectKey_ThenReturnNotFound() throws InvalidIdException, InvalidCatalogItemEntityException { when(authInfo.getCurrentPrincipalName()).thenReturn(principalName); + when(authenticationFacade.getIdToken()).thenReturn("id-token"); when(catalogItemsApiFacade.fetchCatalogItem(any())).thenReturn(null); // When