diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImpl.java b/src/main/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImpl.java index effbf13..185585f 100644 --- a/src/main/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImpl.java +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImpl.java @@ -85,7 +85,7 @@ public Response saveApplicationFormsAndDuosV1(String id, SaveFormsAndDuos saveFo } @Override - public Response submitApplicationV1(String id) { + public Response submitApplicationV1(Long id) { submitApplicationService.submitApplication(id); return Response.noContent().build(); } diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotFoundException.java b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotFoundException.java new file mode 100644 index 0000000..45b6abd --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotFoundException.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.exceptions; + +public class ApplicationNotFoundException extends RuntimeException { + + private static final String MESSAGE = "Application %s not found"; + + public ApplicationNotFoundException(Long applicationId) { + super(MESSAGE.formatted(applicationId)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotInCorrectStateException.java b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotInCorrectStateException.java new file mode 100644 index 0000000..174c8e3 --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/ApplicationNotInCorrectStateException.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.exceptions; + +public class ApplicationNotInCorrectStateException extends RuntimeException { + + private static final String MESSAGE = "Application %s is not in correct state: %s"; + + public ApplicationNotInCorrectStateException(Long id, String state) { + super(MESSAGE.formatted(id, state)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/UserNotApplicantException.java b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/UserNotApplicantException.java new file mode 100644 index 0000000..1a1e108 --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/exceptions/UserNotApplicantException.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.exceptions; + +public class UserNotApplicantException extends RuntimeException { + + private static final String MESSAGE = "User %s is not an applicant for application %s"; + + public UserNotApplicantException(Long applicationId, String userId) { + super(MESSAGE.formatted(userId, applicationId)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotFoundExceptionMapper.java b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotFoundExceptionMapper.java new file mode 100644 index 0000000..44eeafb --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotFoundExceptionMapper.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.mappers; +import io.github.genomicdatainfrastructure.daam.exceptions.ApplicationNotFoundException; +import io.github.genomicdatainfrastructure.daam.model.ErrorResponse; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +@Provider +public class ApplicationNotFoundExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ApplicationNotFoundException exception) { + ErrorResponse errorResponse = new ErrorResponse( + "Application Not Found", + Response.Status.NOT_FOUND.getStatusCode(), + exception.getMessage() + ); + + return Response + .status(Response.Status.NOT_FOUND) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotInCorrectStateExceptionMapper.java b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotInCorrectStateExceptionMapper.java new file mode 100644 index 0000000..d26a9da --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/ApplicationNotInCorrectStateExceptionMapper.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.mappers; + +import io.github.genomicdatainfrastructure.daam.exceptions.ApplicationNotFoundException; +import io.github.genomicdatainfrastructure.daam.exceptions.ApplicationNotInCorrectStateException; +import io.github.genomicdatainfrastructure.daam.model.ErrorResponse; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class ApplicationNotInCorrectStateExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ApplicationNotInCorrectStateException exception) { + ErrorResponse errorResponse = new ErrorResponse( + "Application Not In Correct State", + Response.Status.PRECONDITION_REQUIRED.getStatusCode(), + exception.getMessage() + ); + + return Response + .status(Response.Status.PRECONDITION_REQUIRED) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/UserNotApplicantExceptionMapper.java b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/UserNotApplicantExceptionMapper.java new file mode 100644 index 0000000..b45a85f --- /dev/null +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/mappers/UserNotApplicantExceptionMapper.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 PNED G.I.E. +// +// SPDX-License-Identifier: Apache-2.0 +package io.github.genomicdatainfrastructure.daam.mappers; + +import io.github.genomicdatainfrastructure.daam.exceptions.UserNotApplicantException; +import io.github.genomicdatainfrastructure.daam.model.ErrorResponse; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class UserNotApplicantExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(UserNotApplicantException exception) { + + ErrorResponse errorResponse = new ErrorResponse( + "User Not Applicant", + Response.Status.FORBIDDEN.getStatusCode(), + exception.getMessage() + ); + + return Response + .status(Response.Status.FORBIDDEN) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/services/ListApplicationsService.java b/src/main/java/io/github/genomicdatainfrastructure/daam/services/ListApplicationsService.java index 53657f9..6bbd3b4 100644 --- a/src/main/java/io/github/genomicdatainfrastructure/daam/services/ListApplicationsService.java +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/services/ListApplicationsService.java @@ -37,7 +37,7 @@ public ListedApplication parse(ApplicationOverview applicationOverview) { return ListedApplication.builder() .id(applicationOverview.getApplicationId().toString()) .title(applicationOverview.getApplicationExternalId()) - .currentState(applicationOverview.getApplicationState()) + .currentState(applicationOverview.getApplicationState().value()) .stateChangedAt(applicationOverview.getApplicationLastActivity()) .build(); } diff --git a/src/main/java/io/github/genomicdatainfrastructure/daam/services/SubmitApplicationService.java b/src/main/java/io/github/genomicdatainfrastructure/daam/services/SubmitApplicationService.java index d1e2565..57b97ff 100644 --- a/src/main/java/io/github/genomicdatainfrastructure/daam/services/SubmitApplicationService.java +++ b/src/main/java/io/github/genomicdatainfrastructure/daam/services/SubmitApplicationService.java @@ -5,15 +5,23 @@ package io.github.genomicdatainfrastructure.daam.services; import static io.github.genomicdatainfrastructure.daam.security.PostAuthenticationFilter.USER_ID_CLAIM; + +import io.github.genomicdatainfrastructure.daam.exceptions.ApplicationNotFoundException; +import io.github.genomicdatainfrastructure.daam.exceptions.ApplicationNotInCorrectStateException; +import io.github.genomicdatainfrastructure.daam.exceptions.UserNotApplicantException; import io.github.genomicdatainfrastructure.daam.remote.rems.api.RemsApplicationCommandApi; +import io.github.genomicdatainfrastructure.daam.remote.rems.api.RemsApplicationsApi; import io.github.genomicdatainfrastructure.daam.remote.rems.model.SubmitApplicationCommand; +import io.github.genomicdatainfrastructure.daam.remote.rems.model.ApplicationOverview.ApplicationStateEnum; import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; +import jakarta.ws.rs.WebApplicationException; +import java.util.Set; @ApplicationScoped public class SubmitApplicationService { @@ -21,26 +29,56 @@ public class SubmitApplicationService { private final SecurityIdentity identity; private final String remsApiKey; private final RemsApplicationCommandApi remsApplicationCommandApi; + private final RemsApplicationsApi remsApplicationsApi; + + private static final Set STATES_FORBIDDEN_FOR_SUBMISSION = Set.of(ApplicationStateEnum.DRAFT, ApplicationStateEnum.RETURNED); @Inject public SubmitApplicationService( @ConfigProperty(name = "quarkus.rest-client.rems_yaml.api-key") String remsApiKey, SecurityIdentity identity, - @RestClient RemsApplicationCommandApi applicationsApi + @RestClient RemsApplicationCommandApi applicationCommandApi, + @RestClient RemsApplicationsApi applicationsApi ) { this.remsApiKey = remsApiKey; this.identity = identity; - this.remsApplicationCommandApi = applicationsApi; + this.remsApplicationCommandApi = applicationCommandApi; + this.remsApplicationsApi = applicationsApi; } - public void submitApplication(String id) { + public void submitApplication(Long id) { var principal = (OidcJwtCallerPrincipal) identity.getPrincipal(); String userId = principal.getClaim(USER_ID_CLAIM); + checkApplication(id, userId); + SubmitApplicationCommand command = SubmitApplicationCommand.builder() - .applicationId(Long.valueOf(id)) + .applicationId(id) .build(); remsApplicationCommandApi.apiApplicationsSubmitPost(command, remsApiKey, userId); } + + private void checkApplication(Long id, String userId) { + try { + var application = remsApplicationsApi.apiApplicationsApplicationIdGet(id, remsApiKey, userId); + + if (!application.getApplicationApplicant().getUserid().equals(userId)) { + throw new UserNotApplicantException(id, userId); + } + + if (!STATES_FORBIDDEN_FOR_SUBMISSION.contains(application.getApplicationState())) { + throw new ApplicationNotInCorrectStateException(id, application.getApplicationState().value()); + } + + } catch (WebApplicationException e) { + if (e.getResponse().getStatus() == 404) { + throw new ApplicationNotFoundException(id); + } + + throw e; + } + + } + } diff --git a/src/main/openapi/daam.yaml b/src/main/openapi/daam.yaml index b843f4e..3282f28 100644 --- a/src/main/openapi/daam.yaml +++ b/src/main/openapi/daam.yaml @@ -112,7 +112,8 @@ paths: description: ID of application to submit required: true schema: - type: string + type: integer + format: int64 responses: "204": description: Successful Response (no content) @@ -124,6 +125,25 @@ paths: type: array items: $ref: "#/components/schemas/ValidationWarnings" + "404": + description: Application not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "403": + description: Application does not belong to applicant + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "428": + description: Application not in submittable state + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + security: - daam_auth: - write:applications @@ -823,3 +843,14 @@ components: title: dataset ids items: type: string + ErrorResponse: + properties: + title: + type: string + title: Error title + status: + type: integer + title: Error status + detail: + type: string + title: Error detail \ No newline at end of file diff --git a/src/main/openapi/rems.yaml b/src/main/openapi/rems.yaml index 62577f0..1a46793 100644 --- a/src/main/openapi/rems.yaml +++ b/src/main/openapi/rems.yaml @@ -70,6 +70,36 @@ paths: type: array items: $ref: '#/components/schemas/ApplicationOverview' + /api/applications/{application-id}: + get: + tags: + - rems-applications + summary: 'Get application details (roles: logged-in)' + parameters: + - name: x-rems-api-key + in: header + description: REMS API-Key (optional for UI, required for API) + schema: + type: string + - name: x-rems-user-id + in: header + description: user (optional for UI, required for API). This can be a REMS internal or an external user identity attribute (specified in config.edn). + schema: + type: string + - name: application-id + in: path + description: Application id + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationOverview' /api/applications/create: post: tags: @@ -274,6 +304,14 @@ components: format: date-time application/state: type: string + enum: + - application.state/draft + - application.state/closed + - application.state/approved + - application.state/returned + - application.state/rejected + - application.state/revoked + - application.state/submitted application/copied-to: type: array items: diff --git a/src/test/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImplTest.java b/src/test/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImplTest.java index 682de1d..aa5b090 100644 --- a/src/test/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImplTest.java +++ b/src/test/java/io/github/genomicdatainfrastructure/daam/api/ApplicationCommandApiImplTest.java @@ -6,7 +6,6 @@ import io.github.genomicdatainfrastructure.daam.model.CreateApplication; import org.junit.jupiter.api.Test; -import static org.hamcrest.Matchers.equalTo; import io.quarkus.test.junit.QuarkusTest; import static io.restassured.RestAssured.given; import io.quarkus.test.keycloak.client.KeycloakTestClient; @@ -52,6 +51,40 @@ void submitApplication_when_authenticated() { .statusCode(204); } + @Test + void submitApplication_when_application_not_found() { + given() + .auth() + .oauth2(getAccessToken("alice")) + .when() + .post("/api/v1/applications/12345/submit") + .then() + .statusCode(404); + } + + @Test + void submitApplication_when_not_applicant() { + given() + .auth() + .oauth2(getAccessToken("jdoe")) + .when() + .post("/api/v1/applications/1/submit") + .then() + .statusCode(403); + } + + @Test + void submitApplication_when_application_not_in_submittable_state() { + given() + .auth() + .oauth2(getAccessToken("alice")) + .when() + .post("/api/v1/applications/2/submit") + .then() + .statusCode(428); + } + + private String getAccessToken(String userName) { return keycloakClient.getAccessToken(userName); } diff --git a/src/test/resources/mappings/get_application.json b/src/test/resources/mappings/get_application.json new file mode 100644 index 0000000..a767121 --- /dev/null +++ b/src/test/resources/mappings/get_application.json @@ -0,0 +1,69 @@ +{ + "request": { + "method": "GET", + "url": "/api/applications/1" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "application/workflow": { + "workflow/id": 4, + "workflow/type": "workflow/master" + }, + "application/external-id": "2024/14", + "application/blacklist": [], + "application/id": 1, + "application/applicant": { + "userid": "eb4123a3-b722-4798-9af5-8957f823657a", + "name": "alice", + "email": null + }, + "application/todo": null, + "application/members": [], + "application/resources": [ + { + "catalogue-item/end": null, + "catalogue-item/expired": false, + "catalogue-item/enabled": true, + "resource/id": 1, + "catalogue-item/title": { + "en": "Auto-approve workflow" + }, + "catalogue-item/infourl": { + "en": "http://www.google.com" + }, + "resource/ext-id": "urn:nbn:fi:lb-201403262", + "catalogue-item/start": "2024-03-05T15:16:17.176Z", + "catalogue-item/archived": false, + "catalogue-item/id": 9 + } + ], + "application/accepted-licenses": {}, + "application/invited-members": [], + "application/description": "", + "application/generated-external-id": "2024/14", + "application/permissions": [ + "application.command/copy-as-new", + "application.command/invite-member", + "application.command/submit", + "application.command/remove-member", + "application.command/accept-licenses", + "application.command/uninvite-member", + "application.command/delete", + "application.command/save-draft", + "application.command/change-resources" + ], + "application/last-activity": "2024-03-05T19:44:46.208Z", + "application/roles": [ + "applicant" + ], + "application/attachments": [], + "application/created": "2024-03-05T19:44:46.208Z", + "application/state": "application.state/draft", + "application/modified": "2024-03-05T19:44:46.208Z" + } + } +} \ No newline at end of file diff --git a/src/test/resources/mappings/get_application.json.license b/src/test/resources/mappings/get_application.json.license new file mode 100644 index 0000000..c8d4da6 --- /dev/null +++ b/src/test/resources/mappings/get_application.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 PNED G.I.E. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/src/test/resources/mappings/get_application_404.json b/src/test/resources/mappings/get_application_404.json new file mode 100644 index 0000000..bd5790c --- /dev/null +++ b/src/test/resources/mappings/get_application_404.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "url": "/api/applications/12345" + }, + "response": { + "status": 404, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "error": "Not Found", + "message": "Application not found" + } + } +} \ No newline at end of file diff --git a/src/test/resources/mappings/get_application_404.json.license b/src/test/resources/mappings/get_application_404.json.license new file mode 100644 index 0000000..c8d4da6 --- /dev/null +++ b/src/test/resources/mappings/get_application_404.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 PNED G.I.E. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/src/test/resources/mappings/get_submitted_application.json b/src/test/resources/mappings/get_submitted_application.json new file mode 100644 index 0000000..696753a --- /dev/null +++ b/src/test/resources/mappings/get_submitted_application.json @@ -0,0 +1,69 @@ +{ + "request": { + "method": "GET", + "url": "/api/applications/2" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "application/workflow": { + "workflow/id": 4, + "workflow/type": "workflow/master" + }, + "application/external-id": "2024/14", + "application/blacklist": [], + "application/id": 2, + "application/applicant": { + "userid": "eb4123a3-b722-4798-9af5-8957f823657a", + "name": "alice", + "email": null + }, + "application/todo": null, + "application/members": [], + "application/resources": [ + { + "catalogue-item/end": null, + "catalogue-item/expired": false, + "catalogue-item/enabled": true, + "resource/id": 1, + "catalogue-item/title": { + "en": "Auto-approve workflow" + }, + "catalogue-item/infourl": { + "en": "http://www.google.com" + }, + "resource/ext-id": "urn:nbn:fi:lb-201403262", + "catalogue-item/start": "2024-03-05T15:16:17.176Z", + "catalogue-item/archived": false, + "catalogue-item/id": 9 + } + ], + "application/accepted-licenses": {}, + "application/invited-members": [], + "application/description": "", + "application/generated-external-id": "2024/14", + "application/permissions": [ + "application.command/copy-as-new", + "application.command/invite-member", + "application.command/submit", + "application.command/remove-member", + "application.command/accept-licenses", + "application.command/uninvite-member", + "application.command/delete", + "application.command/save-draft", + "application.command/change-resources" + ], + "application/last-activity": "2024-03-05T19:44:46.208Z", + "application/roles": [ + "applicant" + ], + "application/attachments": [], + "application/created": "2024-03-05T19:44:46.208Z", + "application/state": "application.state/submitted", + "application/modified": "2024-03-05T19:44:46.208Z" + } + } +} \ No newline at end of file diff --git a/src/test/resources/mappings/get_submitted_application.json.license b/src/test/resources/mappings/get_submitted_application.json.license new file mode 100644 index 0000000..c8d4da6 --- /dev/null +++ b/src/test/resources/mappings/get_submitted_application.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 PNED G.I.E. + +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file