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/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/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/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/controllers/ProvisionerActionsApiController.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java index 02f8b63..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,23 +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.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 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}") @@ -25,8 +23,10 @@ @Slf4j public class ProvisionerActionsApiController implements ProvisionerActionsApi { + private final ProvisionerActionsApiFacade provisionerActionsApiFacade; private final ProvisionerActionsService provisionerActionsService; + @SneakyThrows @Override public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, @@ -36,12 +36,13 @@ public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); + 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(); } @@ -53,6 +54,7 @@ public ResponseEntity notifyProvisioningStatusUpdatePartially(String proje projectKey, provisioningStatusUpdateRequest.toString()); var normalizedProjectKey = projectKey.toUpperCase(); + provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest); var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); @@ -75,10 +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(); - } } 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/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/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 new file mode 100644 index 0000000..52dab48 --- /dev/null +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -0,0 +1,60 @@ +package org.opendevstack.component_catalog.server.facade; + +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.stereotype.Component; + +import java.util.List; + +@Component +@AllArgsConstructor +@Slf4j +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() + .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(authenticationFacade.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"); + } + } + +} \ 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..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 @@ -1,8 +1,14 @@ 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; +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; @@ -10,11 +16,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; @@ -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 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..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 @@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +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; @@ -15,6 +16,7 @@ import java.util.List; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @ExtendWith(SpringExtension.class) @@ -23,6 +25,9 @@ class ProvisionerActionsApiControllerTest { @Mock private ProvisionerActionsService provisionerActionsService; + @Mock + private ProvisionerActionsApiFacade provisionerActionsApiFacade; + @InjectMocks private ProvisionerActionsApiController provisionerActionsApiController; @@ -46,12 +51,15 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro .componentUrl(componentUrl) .parameters(parameters); + 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 @@ -74,20 +82,81 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC .componentUrl(componentUrl) .parameters(parameters); + 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 ); } + @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); + + 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, "", mappedParameters); + } + + @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); + + 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, "", mappedParameters); + } + @Test void givenAProjectKey_whenDeleteProvisioningStatus_thenServiceIsCalled() throws JsonProcessingException { // given @@ -103,4 +172,24 @@ 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()); + } } 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); + } +} 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 new file mode 100644 index 0000000..3cfba44 --- /dev/null +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -0,0 +1,136 @@ +package org.opendevstack.component_catalog.server.facade; + +import org.apache.commons.lang3.tuple.Pair; +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.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 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.when; + +@ExtendWith(MockitoExtension.class) +class ProvisionerActionsApiFacadeTest { + + @Mock + private ProjectsInfoService projectsInfoService; + @Mock + private GroupsRestrictionsEvaluator groupsRestrictionsEvaluator; + @Mock + private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; + @Mock + private AuthenticationFacade authenticationFacade; + @InjectMocks + private ProvisionerActionsApiFacade provisionerActionsApiFacade; + + @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"); + + 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); + 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"); + + 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); + 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"); + } + + @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"); + + 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); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(null, "Unknown")); + + // when / then + provisionerActionsApiFacade.validateGroupRestrictions(projectKey, request); + } +}