Skip to content

Commit fbdcfd4

Browse files
feat(mAPI): add PUT calls to update V4 APIs
1 parent 36d34b0 commit fbdcfd4

File tree

13 files changed

+709
-9
lines changed

13 files changed

+709
-9
lines changed

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/java/io/gravitee/rest/api/management/v2/rest/resource/api/ApiResource.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import static java.util.Collections.emptyList;
2222
import static java.util.Collections.singletonList;
2323

24+
import io.gravitee.apim.core.api.domain_service.OAIDomainService;
25+
import io.gravitee.apim.core.api.exception.InvalidPathsException;
2426
import io.gravitee.apim.core.api.model.UpdateNativeApi;
2527
import io.gravitee.apim.core.api.model.crd.IDExportStrategy;
28+
import io.gravitee.apim.core.api.model.import_definition.ImportDefinition;
2629
import io.gravitee.apim.core.api.model.utils.MigrationResult;
2730
import io.gravitee.apim.core.api.use_case.DetachAutomatedApiUseCase;
2831
import io.gravitee.apim.core.api.use_case.ExportApiCRDUseCase;
@@ -31,6 +34,7 @@
3134
import io.gravitee.apim.core.api.use_case.GetExposedEntrypointsUseCase;
3235
import io.gravitee.apim.core.api.use_case.MigrateApiUseCase;
3336
import io.gravitee.apim.core.api.use_case.RollbackApiUseCase;
37+
import io.gravitee.apim.core.api.use_case.UpdateApiDefinitionUseCase;
3438
import io.gravitee.apim.core.api.use_case.UpdateFederatedApiUseCase;
3539
import io.gravitee.apim.core.api.use_case.UpdateNativeApiUseCase;
3640
import io.gravitee.apim.core.audit.model.AuditActor;
@@ -59,6 +63,8 @@
5963
import io.gravitee.rest.api.management.v2.rest.model.ApiTransferOwnership;
6064
import io.gravitee.rest.api.management.v2.rest.model.DuplicateApiOptions;
6165
import io.gravitee.rest.api.management.v2.rest.model.Error;
66+
import io.gravitee.rest.api.management.v2.rest.model.ExportApiV4;
67+
import io.gravitee.rest.api.management.v2.rest.model.ImportSwaggerDescriptor;
6268
import io.gravitee.rest.api.management.v2.rest.model.MigrationReportResponses;
6369
import io.gravitee.rest.api.management.v2.rest.model.MigrationReportResponsesIssuesInner;
6470
import io.gravitee.rest.api.management.v2.rest.model.MigrationStateType;
@@ -84,6 +90,7 @@
8490
import io.gravitee.rest.api.management.v2.rest.resource.documentation.ApiPagesResource;
8591
import io.gravitee.rest.api.management.v2.rest.resource.param.LifecycleAction;
8692
import io.gravitee.rest.api.management.v2.rest.resource.param.PaginationParam;
93+
import io.gravitee.rest.api.model.ImportSwaggerDescriptorEntity;
8794
import io.gravitee.rest.api.model.InlinePictureEntity;
8895
import io.gravitee.rest.api.model.MembershipMemberType;
8996
import io.gravitee.rest.api.model.RoleEntity;
@@ -127,6 +134,7 @@
127134
import io.gravitee.rest.api.service.v4.ApiLicenseService;
128135
import io.gravitee.rest.api.service.v4.ApiStateService;
129136
import io.gravitee.rest.api.service.v4.ApiWorkflowStateService;
137+
import io.gravitee.rest.api.service.v4.exception.InvalidPathException;
130138
import io.swagger.v3.oas.annotations.parameters.RequestBody;
131139
import jakarta.annotation.Nullable;
132140
import jakarta.inject.Inject;
@@ -247,6 +255,12 @@ public class ApiResource extends AbstractResource {
247255
@Inject
248256
private DetachAutomatedApiUseCase detachAutomatedApiUseCase;
249257

258+
@Inject
259+
private UpdateApiDefinitionUseCase updateApiDefinitionUseCase;
260+
261+
@Inject
262+
private OAIDomainService oaiDomainService;
263+
250264
@Context
251265
protected UriInfo uriInfo;
252266

@@ -322,6 +336,69 @@ public Response getApiById(@PathParam("apiId") String apiId) {
322336
return apiResponse(apiEntity);
323337
}
324338

339+
@PUT
340+
@Path("/_import/definition")
341+
@Consumes(MediaType.APPLICATION_JSON)
342+
@Produces(MediaType.APPLICATION_JSON)
343+
@Permissions({ @Permission(value = RolePermission.API_DEFINITION, acls = RolePermissionAction.UPDATE) })
344+
public Response updateApiWithDefinition(@PathParam("apiId") String apiId, @Valid ExportApiV4 apiToImport) {
345+
verifyApiImage(apiToImport.getApiPicture(), "picture");
346+
verifyApiImage(apiToImport.getApiBackground(), "background");
347+
348+
ImportDefinition importDefinition = ImportExportApiMapper.INSTANCE.toImportDefinition(apiToImport);
349+
350+
try {
351+
var audit = getAuditInfo();
352+
UpdateApiDefinitionUseCase.Output output = updateApiDefinitionUseCase.execute(
353+
new UpdateApiDefinitionUseCase.Input(apiId, importDefinition, audit)
354+
);
355+
356+
boolean isSynchronized = apiStateService.isSynchronized(
357+
GraviteeContext.getExecutionContext(),
358+
getGenericApiEntityById(apiId, false)
359+
);
360+
361+
return Response.ok().entity(ApiMapper.INSTANCE.map(output.apiWithFlows(), uriInfo, isSynchronized)).build();
362+
} catch (InvalidPathsException e) {
363+
throw new InvalidPathException("Cannot import API with invalid paths", e);
364+
}
365+
}
366+
367+
@PUT
368+
@Path("/_import/swagger")
369+
@Consumes(MediaType.APPLICATION_JSON)
370+
@Produces(MediaType.APPLICATION_JSON)
371+
@Permissions({ @Permission(value = RolePermission.API_DEFINITION, acls = RolePermissionAction.UPDATE) })
372+
public Response updateApiFromSwagger(@PathParam("apiId") String apiId, @Valid @NotNull ImportSwaggerDescriptor descriptor) {
373+
try {
374+
var audit = getAuditInfo();
375+
var importSwaggerDescriptor = ImportSwaggerDescriptorEntity.builder()
376+
.payload(descriptor.getPayload())
377+
.withDocumentation(Boolean.TRUE.equals(descriptor.getWithDocumentation()))
378+
.build();
379+
380+
var importDefinition = oaiDomainService.convert(
381+
audit.organizationId(),
382+
audit.environmentId(),
383+
importSwaggerDescriptor,
384+
Boolean.TRUE.equals(descriptor.getWithDocumentation()),
385+
Boolean.TRUE.equals(descriptor.getWithOASValidationPolicy())
386+
);
387+
388+
UpdateApiDefinitionUseCase.Output output = updateApiDefinitionUseCase.execute(
389+
new UpdateApiDefinitionUseCase.Input(apiId, importDefinition, audit)
390+
);
391+
392+
boolean isSynchronized = apiStateService.isSynchronized(
393+
GraviteeContext.getExecutionContext(),
394+
getGenericApiEntityById(apiId, false)
395+
);
396+
return Response.ok().entity(ApiMapper.INSTANCE.map(output.apiWithFlows(), uriInfo, isSynchronized)).build();
397+
} catch (InvalidPathsException e) {
398+
throw new InvalidPathException("Cannot import API with invalid paths", e);
399+
}
400+
}
401+
325402
@PUT
326403
@Consumes(MediaType.APPLICATION_JSON)
327404
@Produces(MediaType.APPLICATION_JSON)
@@ -1169,4 +1246,13 @@ private void assertNoPrimaryOwnerReassignment(String poRole) {
11691246
throw new TransferOwnershipNotAllowedException(poRole);
11701247
}
11711248
}
1249+
1250+
private static void verifyApiImage(String imageContent, String imageUsage) {
1251+
try {
1252+
ImageUtils.verify(imageContent);
1253+
} catch (InvalidImageException e) {
1254+
log.warn("Error while parsing {} while importing api", imageUsage, e);
1255+
throw new BadRequestException("Invalid image format for api " + imageUsage);
1256+
}
1257+
}
11721258
}

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/main/resources/openapi/openapi-apis.yaml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,66 @@ paths:
561561
$ref: "#/components/schemas/ExportApiV4"
562562
default:
563563
$ref: "#/components/responses/Error"
564+
/environments/{envId}/apis/{apiId}/_import/definition:
565+
parameters:
566+
- $ref: "#/components/parameters/envIdParam"
567+
- $ref: "#/components/parameters/apiIdParam"
568+
put:
569+
tags:
570+
- APIs
571+
summary: Update API from definition
572+
description: |-
573+
Update an existing API by importing a Gravitee API definition.<br>
574+
This definition can be retrieved from `GET /environments/{envId}/apis/{apiId}/_export/definition`
575+
576+
User must have the API_DEFINITION[UPDATE] permission.
577+
operationId: updateApiWithDefinition
578+
requestBody:
579+
content:
580+
application/json:
581+
schema:
582+
$ref: "#/components/schemas/ExportApiV4"
583+
required: true
584+
responses:
585+
"200":
586+
description: API successfully updated
587+
content:
588+
application/json:
589+
schema:
590+
$ref: "#/components/schemas/ApiV4"
591+
default:
592+
$ref: "#/components/responses/Error"
593+
594+
/environments/{envId}/apis/{apiId}/_import/swagger:
595+
parameters:
596+
- $ref: "#/components/parameters/envIdParam"
597+
- $ref: "#/components/parameters/apiIdParam"
598+
put:
599+
tags:
600+
- APIs
601+
summary: Update API from OpenAPI descriptor
602+
description: |-
603+
Update an existing API by importing an OpenAPI (Swagger) descriptor.<br>
604+
The descriptor payload must be a valid OpenAPI 2.x or 3.x document in JSON or YAML format.
605+
606+
User must have the API_DEFINITION[UPDATE] permission.
607+
operationId: updateApiFromSwagger
608+
requestBody:
609+
content:
610+
application/json:
611+
schema:
612+
$ref: "#/components/schemas/ImportSwaggerDescriptor"
613+
required: true
614+
responses:
615+
"200":
616+
description: API successfully updated
617+
content:
618+
application/json:
619+
schema:
620+
$ref: "#/components/schemas/ApiV4"
621+
default:
622+
$ref: "#/components/responses/Error"
623+
564624
/environments/{envId}/apis/{apiId}/_start:
565625
parameters:
566626
- $ref: "#/components/parameters/envIdParam"

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/AbstractResourceTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@
3939
import inmemory.UserCrudServiceInMemory;
4040
import inmemory.UserDomainServiceInMemory;
4141
import io.gravitee.apim.core.api.domain_service.CategoryDomainService;
42+
import io.gravitee.apim.core.api.domain_service.OAIDomainService;
4243
import io.gravitee.apim.core.api.domain_service.VerifyApiPathDomainService;
4344
import io.gravitee.apim.core.api.use_case.ExportApiUseCase;
45+
import io.gravitee.apim.core.api.use_case.UpdateApiDefinitionUseCase;
4446
import io.gravitee.apim.core.group.model.Group;
4547
import io.gravitee.apim.core.specgen.use_case.SpecGenRequestUseCase;
4648
import io.gravitee.apim.core.user.model.BaseUserEntity;
@@ -122,6 +124,12 @@ public abstract class AbstractResourceTest extends JerseySpringTest {
122124
@Autowired
123125
protected ExportApiUseCase exportApiUseCase;
124126

127+
@Autowired
128+
protected UpdateApiDefinitionUseCase updateApiDefinitionUseCase;
129+
130+
@Autowired
131+
protected OAIDomainService oaiDomainService;
132+
125133
@Autowired
126134
protected PermissionService permissionService;
127135

gravitee-apim-rest-api/gravitee-apim-rest-api-management-v2/gravitee-apim-rest-api-management-v2-rest/src/test/java/io/gravitee/rest/api/management/v2/rest/resource/api/ApiResourceTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ public void init() throws TechnicalException {
5353
apiWorkflowStateService,
5454
roleService,
5555
apiDuplicatorService,
56-
apiDuplicateService
56+
apiDuplicateService,
57+
updateApiDefinitionUseCase,
58+
oaiDomainService
5759
);
5860
GraviteeContext.cleanContext();
5961

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.management.v2.rest.resource.api;
17+
18+
import static io.gravitee.common.http.HttpStatusCode.BAD_REQUEST_400;
19+
import static io.gravitee.common.http.HttpStatusCode.FORBIDDEN_403;
20+
import static io.gravitee.common.http.HttpStatusCode.NOT_FOUND_404;
21+
import static io.gravitee.common.http.HttpStatusCode.OK_200;
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.Mockito.when;
25+
26+
import fixtures.core.model.ApiFixtures;
27+
import io.gravitee.apim.core.api.exception.ApiNotFoundException;
28+
import io.gravitee.apim.core.api.exception.InvalidPathsException;
29+
import io.gravitee.apim.core.api.model.ApiWithFlows;
30+
import io.gravitee.apim.core.api.model.import_definition.ImportDefinition;
31+
import io.gravitee.apim.core.api.use_case.UpdateApiDefinitionUseCase;
32+
import io.gravitee.rest.api.management.v2.rest.model.ApiV4;
33+
import io.gravitee.rest.api.management.v2.rest.model.ImportSwaggerDescriptor;
34+
import io.gravitee.rest.api.model.permissions.RolePermission;
35+
import io.gravitee.rest.api.model.permissions.RolePermissionAction;
36+
import io.gravitee.rest.api.service.common.GraviteeContext;
37+
import jakarta.ws.rs.client.Entity;
38+
import jakarta.ws.rs.core.MediaType;
39+
import jakarta.ws.rs.core.Response;
40+
import org.junit.jupiter.api.Test;
41+
42+
class ApiResource_UpdateApiFromSwaggerTest extends ApiResourceTest {
43+
44+
@Override
45+
protected String contextPath() {
46+
return "/environments/" + ENVIRONMENT + "/apis/" + API + "/_import/swagger";
47+
}
48+
49+
@Test
50+
void should_return_403_when_no_definition_update_permission() {
51+
when(
52+
permissionService.hasPermission(
53+
GraviteeContext.getExecutionContext(),
54+
RolePermission.API_DEFINITION,
55+
API,
56+
RolePermissionAction.UPDATE
57+
)
58+
).thenReturn(false);
59+
60+
Response response = rootTarget().request().put(Entity.json(new ImportSwaggerDescriptor()));
61+
62+
assertThat(response.getStatus()).isEqualTo(FORBIDDEN_403);
63+
}
64+
65+
@Test
66+
void should_return_404_when_api_not_found() {
67+
when(oaiDomainService.convert(any(), any(), any(), any(Boolean.class), any(Boolean.class))).thenReturn(
68+
ImportDefinition.builder().build()
69+
);
70+
when(updateApiDefinitionUseCase.execute(any())).thenThrow(new ApiNotFoundException(API));
71+
72+
Response response = rootTarget().request().put(Entity.entity(buildDescriptor("{}"), MediaType.APPLICATION_JSON));
73+
74+
assertThat(response.getStatus()).isEqualTo(NOT_FOUND_404);
75+
}
76+
77+
@Test
78+
void should_return_200_and_updated_api_on_success() {
79+
var existingApi = ApiFixtures.aProxyApiV4().toBuilder().id(API).environmentId(ENVIRONMENT).build();
80+
var apiWithFlows = new ApiWithFlows(existingApi, java.util.List.of());
81+
82+
when(oaiDomainService.convert(any(), any(), any(), any(Boolean.class), any(Boolean.class))).thenReturn(
83+
ImportDefinition.builder().build()
84+
);
85+
when(updateApiDefinitionUseCase.execute(any())).thenReturn(new UpdateApiDefinitionUseCase.Output(apiWithFlows));
86+
when(apiSearchServiceV4.findById(GraviteeContext.getExecutionContext(), API)).thenReturn(
87+
io.gravitee.rest.api.model.v4.api.ApiEntity.builder().id(API).name("Test API").apiVersion("1.0").build()
88+
);
89+
90+
Response response = rootTarget().request().put(Entity.entity(buildDescriptor("{}"), MediaType.APPLICATION_JSON));
91+
92+
assertThat(response.getStatus()).isEqualTo(OK_200);
93+
var body = response.readEntity(ApiV4.class);
94+
assertThat(body).isNotNull();
95+
assertThat(body.getId()).isEqualTo(API);
96+
}
97+
98+
@Test
99+
void should_return_400_when_invalid_paths_exception() {
100+
when(oaiDomainService.convert(any(), any(), any(), any(Boolean.class), any(Boolean.class))).thenReturn(
101+
ImportDefinition.builder().build()
102+
);
103+
when(updateApiDefinitionUseCase.execute(any())).thenThrow(new InvalidPathsException("Path [/foo] already exists"));
104+
105+
Response response = rootTarget().request().put(Entity.entity(buildDescriptor("{}"), MediaType.APPLICATION_JSON));
106+
107+
assertThat(response.getStatus()).isEqualTo(BAD_REQUEST_400);
108+
}
109+
110+
private ImportSwaggerDescriptor buildDescriptor(String payload) {
111+
var descriptor = new ImportSwaggerDescriptor();
112+
descriptor.setPayload(payload);
113+
return descriptor;
114+
}
115+
}

0 commit comments

Comments
 (0)