From 5d462dc37b130a107584244ddebd27b8404ade21 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 7 May 2026 17:54:57 +0200 Subject: [PATCH 1/2] Implement reserved parameter validation for project component creation --- .../ProjectComponentsCreateProperties.java | 20 +++++++++ .../ProjectComponentsExceptionHandler.java | 18 +++++++- .../ComponentReservedParamException.java | 8 ++++ .../project/facade/ComponentsFacade.java | 32 ++++++++++++++ ...ProjectComponentsExceptionHandlerTest.java | 16 +++++++ .../project/facade/ComponentsFacadeTest.java | 42 +++++++++++++------ application.yaml | 9 ++++ 7 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentReservedParamException.java diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java new file mode 100644 index 0000000..cf03a62 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java @@ -0,0 +1,20 @@ +package org.opendevstack.apiservice.project.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "apis.project-components.create") +public class ProjectComponentsCreateProperties { + + private List reservedParams = new ArrayList<>( + List.of("workflow", "ods_namespace", "component_type", "quickstarter_repo") + ); +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java index add5c7f..29c4778 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java @@ -9,8 +9,8 @@ import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentErrorKey; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentReservedParamException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; -import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -190,6 +190,22 @@ public ResponseEntity handleComponentBadRequestExceptio return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response); } + @ExceptionHandler(ComponentReservedParamException.class) + public ResponseEntity handleComponentReservedParamException( + ComponentReservedParamException ex, + HttpServletRequest request) { + + log.warn("Reserved parameter sent by client: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.badRequest( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.INVALID_PARAMETERS + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException( Exception ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentReservedParamException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentReservedParamException.java new file mode 100644 index 0000000..8bc9f78 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentReservedParamException.java @@ -0,0 +1,8 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentReservedParamException extends RuntimeException { + + public ComponentReservedParamException(String message) { + super(message); + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java index 3888310..76a5208 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java @@ -8,11 +8,13 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.externalservice.marketplace.service.CatalogItemOperations; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.project.config.ProjectComponentsCreateProperties; import org.opendevstack.apiservice.project.exception.CatalogItemNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentBadRequestException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentReservedParamException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; @@ -21,6 +23,9 @@ import org.springframework.web.client.HttpClientErrorException; import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; @Service @AllArgsConstructor @@ -31,6 +36,8 @@ public class ComponentsFacade { private final MarketplaceMapper marketplaceMapper; + private final ProjectComponentsCreateProperties createProperties; + public Component getProjectComponent(String projectId, String componentId) { try { ProjectComponentExtendedInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); @@ -60,6 +67,7 @@ public Component getProjectComponent(String projectId, String componentId) { } public void provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + validateReservedParams(createComponentRequest); try { CatalogItem catalogItem = resolveCatalogItem(createComponentRequest); List createComponentParameterList = marketplaceMapper @@ -85,6 +93,30 @@ public void provisionProjectComponent(String projectId, CreateComponentRequest c } } + private void validateReservedParams(CreateComponentRequest createComponentRequest) { + if (createComponentRequest == null || createComponentRequest.getParams() == null + || createComponentRequest.getParams().isEmpty()) { + return; + } + + if (createProperties.getReservedParams() == null || createProperties.getReservedParams().isEmpty()) { + return; + } + + Set reservedParams = createProperties.getReservedParams().stream() + .map(param -> param.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + + createComponentRequest.getParams().keySet().stream() + .filter(param -> param != null && reservedParams.contains(param.toLowerCase(Locale.ROOT))) + .findFirst() + .ifPresent(param -> { + throw new ComponentReservedParamException( + String.format("Parameter '%s' cannot be provided because it is managed by the system.", param) + ); + }); + } + /** * Resolves the catalog item that matches the requested {@code productId} (interpreted as the * Marketplace catalog item slug). Returns {@code null} when the request or product id is missing, diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java index 92a0601..268c146 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java @@ -9,6 +9,7 @@ import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentReservedParamException; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; @@ -149,6 +150,21 @@ void handle_component_already_exists_exception_returns_conflict() { assertThat(response.getBody().getMessage()).isEqualTo("This component name already exists, please choose another name."); } + @Test + void handle_component_reserved_param_exception_returns_bad_request() { + ComponentReservedParamException exception = new ComponentReservedParamException( + "Parameter 'ods_namespace' cannot be provided because it is managed by the system."); + + ResponseEntity response = handler.handleComponentReservedParamException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST.name()); + assertThat(response.getBody().getErrorKey()).isEqualTo("006"); + assertThat(response.getBody().getMessage()) + .isEqualTo("Parameter 'ods_namespace' cannot be provided because it is managed by the system."); + } + @Test void handle_generic_exception_returns_internal_server_error() { RuntimeException exception = new RuntimeException("boom"); diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java index b8c7748..65d083e 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java @@ -10,9 +10,11 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.project.config.ProjectComponentsCreateProperties; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentReservedParamException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; @@ -21,6 +23,8 @@ import org.springframework.http.HttpStatus; import org.springframework.web.client.HttpClientErrorException; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; @@ -40,11 +44,15 @@ class ComponentsFacadeTest { @Mock private MarketplaceService marketplaceExternalService; + private ProjectComponentsCreateProperties createProperties; + private ComponentsFacade componentsFacade; @BeforeEach void setup() { - componentsFacade = new ComponentsFacade(marketplaceExternalService, marketplaceMapper); + createProperties = new ProjectComponentsCreateProperties(); + createProperties.setReservedParams(List.of("workflow", "ods_namespace", "component_type", "quickstarter_repo")); + componentsFacade = new ComponentsFacade(marketplaceExternalService, marketplaceMapper, createProperties); } @Test @@ -101,23 +109,33 @@ void create_project_component_throws_creation_exception_when_marketplace_returns verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } - @Test - void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException { + @Test + void create_project_component_throws_bad_request_when_request_contains_reserved_param() { + CreateComponentRequest request = buildTestCreateComponentRequest(); + request.putParamsItem("ods_namespace", "null"); + + assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) + .isInstanceOf(ComponentReservedParamException.class) + .hasMessage("Parameter 'ods_namespace' cannot be provided because it is managed by the system."); + } + + @Test + void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException { CreateComponentRequest request = buildTestCreateComponentRequest(); HttpClientErrorException conflict = HttpClientErrorException.Conflict.create( - HttpStatus.CONFLICT, - "Conflict", - HttpHeaders.EMPTY, - new byte[0], - null + HttpStatus.CONFLICT, + "Conflict", + HttpHeaders.EMPTY, + new byte[0], + null ); when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), anyList())) - .thenThrow(new MarketplaceException("This component name already exists, please choose another name.", conflict)); + .thenThrow(new MarketplaceException("This component name already exists, please choose another name.", conflict)); assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) - .isInstanceOf(ComponentAlreadyExistsException.class) - .hasMessage("This component name already exists, please choose another name."); + .isInstanceOf(ComponentAlreadyExistsException.class) + .hasMessage("This component name already exists, please choose another name."); verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); - } + } } \ No newline at end of file diff --git a/application.yaml b/application.yaml index deb7058..e7fb2e4 100644 --- a/application.yaml +++ b/application.yaml @@ -203,6 +203,15 @@ automation: trust-store-type: ${UIPATH_SSL_TRUST_STORE_TYPE:JKS} apis: + project-components: + create: + # Parameters reserved for backend/system use. If provided by clients in params, request is rejected. + reserved-params: + - "workflow" + - "ods_namespace" + - "component_type" + - "quickstarter_repo" + project-users: # Workflow name triggered for project user automation tasks. ansible-workflow-name: ${API_PROJECT_USERS_WORKFLOW_NAME:ansible++workflow} From a8908afc482b6394c3dfead9fbb5665465394878 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Fri, 8 May 2026 14:40:22 +0200 Subject: [PATCH 2/2] Refactor reserved parameters configuration for project component creation --- .../ProjectComponentsCreateProperties.java | 17 ++++++----------- .../project/facade/ComponentsFacadeTest.java | 5 ++--- application.yaml | 6 +----- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java index cf03a62..bc6eb47 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/ProjectComponentsCreateProperties.java @@ -1,20 +1,15 @@ package org.opendevstack.apiservice.project.config; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Configuration; -import java.util.ArrayList; import java.util.List; -@Getter -@Setter -@Component +@Data +@Configuration @ConfigurationProperties(prefix = "apis.project-components.create") public class ProjectComponentsCreateProperties { - private List reservedParams = new ArrayList<>( - List.of("workflow", "ods_namespace", "component_type", "quickstarter_repo") - ); -} + private List reservedParams; +} \ No newline at end of file diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java index edde099..c7c92a7 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java @@ -15,6 +15,7 @@ import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentDeletionException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentReservedParamException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; @@ -139,9 +140,7 @@ void create_project_component_throws_already_exists_when_marketplace_returns_con .hasMessage("This component name already exists, please choose another name."); verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } - } - - + @Test void delete_project_component_ends_successfully_for_existing_component() throws MarketplaceException { componentsFacade.deleteProjectComponent("testProject", "testComponent"); diff --git a/application.yaml b/application.yaml index e7fb2e4..c17bf8d 100644 --- a/application.yaml +++ b/application.yaml @@ -206,11 +206,7 @@ apis: project-components: create: # Parameters reserved for backend/system use. If provided by clients in params, request is rejected. - reserved-params: - - "workflow" - - "ods_namespace" - - "component_type" - - "quickstarter_repo" + reserved-params: ${API_PROJECT_COMPONENTS_RESERVED_PARAMS:} project-users: # Workflow name triggered for project user automation tasks.