Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.opendevstack.apiservice.project.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Data
@Configuration
@ConfigurationProperties(prefix = "apis.project-components.create")
public class ProjectComponentsCreateProperties {

private List<String> reservedParams;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.opendevstack.apiservice.project.exception.ComponentDeletionException;
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.CreateComponentResponse;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -200,6 +201,22 @@ public ResponseEntity<CreateComponentResponse> handleComponentBadRequestExceptio
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response);
}

@ExceptionHandler(ComponentReservedParamException.class)
public ResponseEntity<CreateComponentResponse> 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<CreateComponentResponse> handleGenericException(
Exception ex,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.opendevstack.apiservice.project.exception;

public class ComponentReservedParamException extends RuntimeException {

public ComponentReservedParamException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
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.ComponentDeletionException;
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;
Expand All @@ -22,6 +24,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
Expand All @@ -32,6 +37,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);
Expand Down Expand Up @@ -61,6 +68,7 @@ public Component getProjectComponent(String projectId, String componentId) {
}

public void provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
validateReservedParams(createComponentRequest);
try {
CatalogItem catalogItem = resolveCatalogItem(createComponentRequest);
List<ProvisionActionParameter> createComponentParameterList = marketplaceMapper
Expand All @@ -86,6 +94,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()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we haven't even configure the createProperties (it should check if it is null or it will throw a NPE

return;
}

Set<String> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<CreateComponentResponse> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
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.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;
Expand All @@ -22,6 +24,8 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;

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.anyList;
Expand All @@ -42,11 +46,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
Expand Down Expand Up @@ -103,27 +111,36 @@ 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());
}


}

@Test
void delete_project_component_ends_successfully_for_existing_component() throws MarketplaceException {
componentsFacade.deleteProjectComponent("testProject", "testComponent");
Expand Down
5 changes: 5 additions & 0 deletions application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ 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: ${API_PROJECT_COMPONENTS_RESERVED_PARAMS:}

project-users:
# Workflow name triggered for project user automation tasks.
ansible-workflow-name: ${API_PROJECT_USERS_WORKFLOW_NAME:ansible++workflow}
Expand Down
Loading