Skip to content

Commit

Permalink
refactor: provide the sanitizer/validator to domain service creation
Browse files Browse the repository at this point in the history
CreateApiDomainService's create method applies a validation automatically.
We will need to use a different validation process for a new type of API
(Federated API).

I didn't want to mix validation of different API types, so I added a
parameter to the create method to provide the validation process as a
lambda function.
  • Loading branch information
jgiovaresco committed Mar 28, 2024
1 parent 727d48c commit 1765f47
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 46 deletions.
Expand Up @@ -27,7 +27,7 @@
import io.gravitee.apim.core.audit.model.event.ApiAuditEvent;
import io.gravitee.apim.core.flow.crud_service.FlowCrudService;
import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerDomainService;
import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerFactory;
import io.gravitee.apim.core.membership.model.PrimaryOwnerEntity;
import io.gravitee.apim.core.notification.crud_service.NotificationConfigCrudService;
import io.gravitee.apim.core.notification.model.config.NotificationConfig;
import io.gravitee.apim.core.parameters.model.ParameterContext;
Expand All @@ -37,15 +37,14 @@
import io.gravitee.rest.api.model.parameters.Key;
import io.gravitee.rest.api.model.parameters.ParameterReferenceType;
import java.util.Collections;
import java.util.function.UnaryOperator;

@DomainService
public class CreateApiDomainService {

private final ValidateApiDomainService validateApiDomainService;
private final ApiCrudService apiCrudService;
private final AuditDomainService auditService;
private final ApiIndexerDomainService apiIndexerDomainService;
private final ApiPrimaryOwnerFactory apiPrimaryOwnerFactory;
private final ApiPrimaryOwnerDomainService apiPrimaryOwnerDomainService;
private final ApiMetadataDomainService apiMetadataDomainService;
private final FlowCrudService flowCrudService;
Expand All @@ -54,23 +53,19 @@ public class CreateApiDomainService {
private final WorkflowCrudService workflowCrudService;

public CreateApiDomainService(
ValidateApiDomainService validateApiDomainService,
ApiCrudService apiCrudService,
AuditDomainService auditService,
ApiIndexerDomainService apiIndexerDomainService,
ApiMetadataDomainService apiMetadataDomainService,
ApiPrimaryOwnerFactory apiPrimaryOwnerFactory,
ApiPrimaryOwnerDomainService apiPrimaryOwnerDomainService,
FlowCrudService flowCrudService,
NotificationConfigCrudService notificationConfigCrudService,
ParametersQueryService parametersQueryService,
WorkflowCrudService workflowCrudService
) {
this.validateApiDomainService = validateApiDomainService;
this.apiCrudService = apiCrudService;
this.auditService = auditService;
this.apiIndexerDomainService = apiIndexerDomainService;
this.apiPrimaryOwnerFactory = apiPrimaryOwnerFactory;
this.apiPrimaryOwnerDomainService = apiPrimaryOwnerDomainService;
this.apiMetadataDomainService = apiMetadataDomainService;
this.flowCrudService = flowCrudService;
Expand All @@ -87,25 +82,15 @@ public CreateApiDomainService(
* <p>
* Once created, the API is indexed in the search engine.
* </p>
* <p>⚠️ There is no validation, callers should ensure the API provided is valid. They can use {@link io.gravitee.apim.core.api.model.factory.ApiModelFactory} to build a valid model.</p>
*
* @param api The API to create.
* @param auditInfo The audit information.
* @param api The API to create.
* @param primaryOwner The primary owner of the API.
* @param auditInfo The audit information.
* @param sanitizer A sanitizer function to apply on the API before creating it. This will be used to apply additional validation and sanitization.
* @return The created API.
*/
public ApiWithFlows create(Api api, AuditInfo auditInfo) {
var primaryOwner = apiPrimaryOwnerFactory.createForNewApi(
auditInfo.organizationId(),
auditInfo.environmentId(),
auditInfo.actor().userId()
);

var sanitized = validateApiDomainService.validateAndSanitizeForCreation(
api,
primaryOwner,
auditInfo.environmentId(),
auditInfo.organizationId()
);
public ApiWithFlows create(Api api, PrimaryOwnerEntity primaryOwner, AuditInfo auditInfo, UnaryOperator<Api> sanitizer) {
var sanitized = sanitizer.apply(api);

var created = apiCrudService.create(sanitized);

Expand Down
Expand Up @@ -54,15 +54,19 @@ public Output execute(Input input) {
auditInfo.actor().userId()
);

var sanitized = validateApiDomainService.validateAndSanitizeForCreation(
var created = createApiDomainService.create(
ApiModelFactory.fromNewApi(input.newApi, auditInfo.environmentId()),
primaryOwner,
auditInfo.environmentId(),
auditInfo.organizationId()
auditInfo,
api ->
validateApiDomainService.validateAndSanitizeForCreation(
api,
primaryOwner,
auditInfo.environmentId(),
auditInfo.organizationId()
)
);

var created = createApiDomainService.create(sanitized, auditInfo);

return new Output(created);
}
}
Expand Up @@ -3,7 +3,7 @@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* You may obtain api copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
Expand All @@ -23,13 +23,16 @@
import io.gravitee.apim.core.api.domain_service.CreateApiDomainService;
import io.gravitee.apim.core.api.domain_service.DeployApiDomainService;
import io.gravitee.apim.core.api.domain_service.UpdateApiDomainService;
import io.gravitee.apim.core.api.domain_service.ValidateApiDomainService;
import io.gravitee.apim.core.api.model.Api;
import io.gravitee.apim.core.api.model.crd.ApiCRD;
import io.gravitee.apim.core.api.model.crd.ApiCRDStatus;
import io.gravitee.apim.core.api.model.crd.PlanCRD;
import io.gravitee.apim.core.api.model.factory.ApiModelFactory;
import io.gravitee.apim.core.api.query_service.ApiQueryService;
import io.gravitee.apim.core.audit.model.AuditInfo;
import io.gravitee.apim.core.exception.AbstractDomainException;
import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerFactory;
import io.gravitee.apim.core.plan.domain_service.CreatePlanDomainService;
import io.gravitee.apim.core.plan.domain_service.DeletePlanDomainService;
import io.gravitee.apim.core.plan.domain_service.ReorderPlanDomainService;
Expand All @@ -53,6 +56,8 @@
public class ImportCRDUseCase {

private final ApiQueryService apiQueryService;
private final ApiPrimaryOwnerFactory apiPrimaryOwnerFactory;
private final ValidateApiDomainService validateApiDomainService;
private final CreateApiDomainService createApiDomainService;
private final CreatePlanDomainService createPlanDomainService;
private final ApiMetadataDomainService apiMetadataDomainService;
Expand All @@ -69,6 +74,8 @@ public class ImportCRDUseCase {
public ImportCRDUseCase(
ApiCrudService apiCrudService,
ApiQueryService apiQueryService,
ApiPrimaryOwnerFactory apiPrimaryOwnerFactory,
ValidateApiDomainService validateApiDomainService,
CreateApiDomainService createApiDomainService,
CreatePlanDomainService createPlanDomainService,
ApiMetadataDomainService apiMetadataDomainService,
Expand All @@ -83,6 +90,8 @@ public ImportCRDUseCase(
) {
this.apiCrudService = apiCrudService;
this.apiQueryService = apiQueryService;
this.apiPrimaryOwnerFactory = apiPrimaryOwnerFactory;
this.validateApiDomainService = validateApiDomainService;
this.createApiDomainService = createApiDomainService;
this.createPlanDomainService = createPlanDomainService;
this.apiMetadataDomainService = apiMetadataDomainService;
Expand Down Expand Up @@ -113,8 +122,15 @@ private ApiCRDStatus create(Input input) {
String environmentId = input.auditInfo.environmentId();
String organizationId = input.auditInfo.organizationId();

var api = createApiDomainService.create(ApiModelFactory.fromCrd(input.crd, environmentId), input.auditInfo);
apiMetadataDomainService.saveApiMetadata(api.getId(), input.crd.getMetadata(), input.auditInfo);
var primaryOwner = apiPrimaryOwnerFactory.createForNewApi(organizationId, environmentId, input.auditInfo.actor().userId());

var createdApi = createApiDomainService.create(
ApiModelFactory.fromCrd(input.crd, environmentId),
primaryOwner,
input.auditInfo,
api -> validateApiDomainService.validateAndSanitizeForCreation(api, primaryOwner, environmentId, organizationId)
);
apiMetadataDomainService.saveApiMetadata(createdApi.getId(), input.crd.getMetadata(), input.auditInfo);

var planNameIdMapping = input.crd
.getPlans()
Expand All @@ -124,25 +140,27 @@ private ApiCRDStatus create(Input input) {
Map.entry(
entry.getKey(),
createPlanDomainService
.create(initPlanFromCRD(entry.getValue()), entry.getValue().getFlows(), api, input.auditInfo)
.create(initPlanFromCRD(entry.getValue()), entry.getValue().getFlows(), createdApi, input.auditInfo)
.getId()
)
)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

if (input.crd.getDefinitionContext().getSyncFrom().equals(DefinitionContext.ORIGIN_MANAGEMENT)) {
deployApiDomainService.deploy(api, "Import via Kubernetes operator", input.auditInfo);
deployApiDomainService.deploy(createdApi, "Import via Kubernetes operator", input.auditInfo);
}

return ApiCRDStatus
.builder()
.id(api.getId())
.crossId(api.getCrossId())
.id(createdApi.getId())
.crossId(createdApi.getCrossId())
.environmentId(environmentId)
.organizationId(organizationId)
.state(api.getLifecycleState().name())
.state(createdApi.getLifecycleState().name())
.plans(planNameIdMapping)
.build();
} catch (AbstractDomainException e) {
throw e;
} catch (Exception e) {
throw new TechnicalManagementException(e);
}
Expand Down
Expand Up @@ -19,11 +19,13 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.gravitee.apim.core.api.domain_service.CreateApiDomainService;
import io.gravitee.apim.core.api.domain_service.ValidateApiDomainService;
import io.gravitee.apim.core.api.model.Api;
import io.gravitee.apim.core.api.model.NewApi;
import io.gravitee.apim.core.api.model.factory.ApiModelFactory;
import io.gravitee.apim.core.audit.model.AuditActor;
import io.gravitee.apim.core.audit.model.AuditInfo;
import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerFactory;
import io.gravitee.definition.jackson.datatype.GraviteeMapper;
import io.gravitee.rest.api.model.ApiMetadataEntity;
import io.gravitee.rest.api.model.Visibility;
Expand Down Expand Up @@ -52,17 +54,23 @@ public class V4ApiServiceCockpitImpl implements V4ApiServiceCockpit {
public static final String PLAN_ENTITIES_NODE = "/planEntities";
public static final String METADATA_NODE = "/metadata";

private final ApiPrimaryOwnerFactory apiPrimaryOwnerFactory;
private final ValidateApiDomainService validateApiDomainService;
private final CreateApiDomainService createApiDomainService;
private final ApiService apiServiceV4;
private final ApiStateService apiStateService;
private final GraviteeMapper graviteeMapper;
private final ObjectMapper mapper;

public V4ApiServiceCockpitImpl(
ApiPrimaryOwnerFactory apiPrimaryOwnerFactory,
ValidateApiDomainService validateApiDomainService,
CreateApiDomainService createApiDomainService,
ApiService apiServiceV4,
ApiStateService apiStateService
) {
this.apiPrimaryOwnerFactory = apiPrimaryOwnerFactory;
this.validateApiDomainService = validateApiDomainService;
this.createApiDomainService = createApiDomainService;
this.apiServiceV4 = apiServiceV4;
this.apiStateService = apiStateService;
Expand All @@ -80,12 +88,15 @@ public Single<ApiEntity> createPublishApi(
final JsonNode node = mapper.readTree(apiDefinition);
final UpdateApiEntity updateApiEntity = getUpdateApiEntity(node);
final ExecutionContext executionContext = new ExecutionContext(organizationId, environmentId);
var primaryOwner = apiPrimaryOwnerFactory.createForNewApi(organizationId, environmentId, userId);

return Single
.just(
createApiDomainService.create(
deserializeApi(node, environmentId),
new AuditInfo(organizationId, environmentId, AuditActor.builder().userId(userId).build())
primaryOwner,
new AuditInfo(organizationId, environmentId, AuditActor.builder().userId(userId).build()),
api -> validateApiDomainService.validateAndSanitizeForCreation(api, primaryOwner, environmentId, organizationId)
)
)
.flatMap(api -> publishApi(executionContext, api, userId, updateApiEntity))
Expand Down
Expand Up @@ -146,15 +146,13 @@ void setUp() {
);

var createApiDomainService = new CreateApiDomainService(
validateApiDomainService,
apiCrudService,
auditService,
new ApiIndexerDomainService(
new ApiMetadataDecoderDomainService(metadataQueryService, new FreemarkerTemplateProcessor()),
indexer
),
new ApiMetadataDomainService(metadataCrudService, auditService),
apiPrimaryOwnerFactory,
new ApiPrimaryOwnerDomainService(
auditService,
groupQueryService,
Expand Down
Expand Up @@ -20,10 +20,12 @@
import static fixtures.core.model.PlanFixtures.aKeylessV4;
import static fixtures.core.model.PlanFixtures.anApiKeyV4;
import static fixtures.core.model.SubscriptionFixtures.aSubscription;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import fixtures.core.model.AuditInfoFixtures;
Expand Down Expand Up @@ -61,13 +63,15 @@
import io.gravitee.apim.core.api.domain_service.CreateApiDomainService;
import io.gravitee.apim.core.api.domain_service.DeployApiDomainService;
import io.gravitee.apim.core.api.domain_service.UpdateApiDomainService;
import io.gravitee.apim.core.api.domain_service.ValidateApiDomainService;
import io.gravitee.apim.core.api.model.Api;
import io.gravitee.apim.core.api.model.crd.ApiCRD;
import io.gravitee.apim.core.api.model.crd.ApiCRDStatus;
import io.gravitee.apim.core.api.model.crd.PlanCRD;
import io.gravitee.apim.core.api_key.domain_service.RevokeApiKeyDomainService;
import io.gravitee.apim.core.audit.domain_service.AuditDomainService;
import io.gravitee.apim.core.audit.model.AuditInfo;
import io.gravitee.apim.core.exception.ValidationDomainException;
import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService;
import io.gravitee.apim.core.group.model.Group;
import io.gravitee.apim.core.membership.domain_service.ApiPrimaryOwnerDomainService;
Expand Down Expand Up @@ -159,6 +163,7 @@ class ImportCRDUseCaseTest {
UserCrudServiceInMemory userCrudService = new UserCrudServiceInMemory();
WorkflowCrudServiceInMemory workflowCrudService = new WorkflowCrudServiceInMemory();

ValidateApiDomainService validateApiDomainService = mock(ValidateApiDomainService.class);
PlanSynchronizationService planSynchronizationService = mock(PlanSynchronizationService.class);
PlanQueryServiceInMemory planQueryService = new PlanQueryServiceInMemory(planCrudService);
IndexerInMemory indexer = new IndexerInMemory();
Expand Down Expand Up @@ -237,15 +242,13 @@ void setUp() {
);

var createApiDomainService = new CreateApiDomainService(
(api, primaryOwner, environmentId, organizationId) -> api,
apiCrudService,
auditDomainService,
new ApiIndexerDomainService(
new ApiMetadataDecoderDomainService(metadataQueryService, new FreemarkerTemplateProcessor()),
indexer
),
new ApiMetadataDomainService(metadataCrudService, auditDomainService),
apiPrimaryOwnerFactory,
new ApiPrimaryOwnerDomainService(
auditDomainService,
groupQueryService,
Expand All @@ -267,6 +270,8 @@ void setUp() {
new ImportCRDUseCase(
apiCrudService,
apiQueryService,
apiPrimaryOwnerFactory,
validateApiDomainService,
createApiDomainService,
createPlanDomainService,
apiMetadataDomainService,
Expand All @@ -290,6 +295,8 @@ void setUp() {
when(policyValidationDomainService.validateAndSanitizeConfiguration(any(), any()))
.thenAnswer(invocation -> invocation.getArgument(1));
when(planSynchronizationService.checkSynchronized(any(), any(), any(), any())).thenReturn(true);
when(validateApiDomainService.validateAndSanitizeForCreation(any(), any(), any(), any()))
.thenAnswer(invocation -> invocation.getArgument(0));

roleQueryService.resetSystemRoles(ORGANIZATION_ID);
givenExistingUsers(
Expand Down Expand Up @@ -324,6 +331,22 @@ void tearDown() {
@Nested
class Create {

@Test
void should_not_create_api_if_validation_fails() {
// Given
when(validateApiDomainService.validateAndSanitizeForCreation(any(), any(), any(), any()))
.thenThrow(new ValidationDomainException("Validation error"));

// When
var throwable = catchThrowable(() -> useCase.execute(new ImportCRDUseCase.Input(AUDIT_INFO, aCRD())));

// Then
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(throwable).isInstanceOf(ValidationDomainException.class);
soft.assertThat(apiCrudService.storage()).isEmpty();
});
}

@Test
void should_create_and_index_a_new_api() {
// Given
Expand Down

0 comments on commit 1765f47

Please sign in to comment.