From 0cb5fc9ff3f1303e6cf636668bcc5d7841c5d8d6 Mon Sep 17 00:00:00 2001 From: Nima Saboonchi Date: Thu, 23 Nov 2023 10:38:54 +0000 Subject: [PATCH] DSSINTER-1044: Custom header check added for CSRF protection + Some OpenAPI and Javadoc updates --- signserver/doc/openapi.json | 132 ++++++++++--- signserver/doc/openapi.yaml | 94 +++++++--- .../cli/defaultimpl/RESTDocumentSigner.java | 1 + .../signserver/common/ForbiddenException.java | 26 +++ .../exception/ForbiddenExceptionMapper.java | 35 ++++ .../rest/api/resource/WorkerResource.java | 177 +++++++++++++----- .../org/signserver/rest/RestWorkersTest.java | 58 +++++- 7 files changed, 427 insertions(+), 96 deletions(-) mode change 100644 => 100755 signserver/doc/openapi.json create mode 100644 signserver/modules/SignServer-Common/src/main/java/org/signserver/common/ForbiddenException.java create mode 100644 signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/exception/ForbiddenExceptionMapper.java diff --git a/signserver/doc/openapi.json b/signserver/doc/openapi.json old mode 100644 new mode 100755 index 369319d8f..08f4c0396 --- a/signserver/doc/openapi.json +++ b/signserver/doc/openapi.json @@ -24,6 +24,12 @@ "required" : true }, "responses" : { + "201" : { + "description" : "Worker added successfully", + "content" : { + "application/json" : { } + } + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -34,6 +40,16 @@ } } }, + "403" : { + "description" : "Access is forbidden!", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } + } + }, "409" : { "description" : "Worker already exists.", "content" : { @@ -53,12 +69,6 @@ } } } - }, - "201" : { - "description" : "Worker added successfully", - "content" : { - "application/json" : { } - } } } } @@ -78,6 +88,12 @@ } }, "responses" : { + "200" : { + "description" : "Workers successfully reloaded", + "content" : { + "application/json" : { } + } + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -88,6 +104,16 @@ } } }, + "403" : { + "description" : "Access is forbidden!", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } + } + }, "404" : { "description" : "No such worker", "content" : { @@ -152,8 +178,8 @@ } } }, - "404" : { - "description" : "No such worker", + "403" : { + "description" : "Access is forbidden!", "content" : { "application/json" : { "schema" : { @@ -162,8 +188,8 @@ } } }, - "503" : { - "description" : "Crypto Token not available", + "404" : { + "description" : "No such worker", "content" : { "application/json" : { "schema" : { @@ -182,6 +208,16 @@ } } }, + "503" : { + "description" : "Crypto Token not available", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } + } + }, "200" : { "description" : "The response data", "content" : { @@ -220,6 +256,12 @@ "required" : true }, "responses" : { + "200" : { + "description" : "Properties replaced successfully", + "content" : { + "application/json" : { } + } + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -230,8 +272,8 @@ } } }, - "500" : { - "description" : "The server were unable to process the request. See server-side logs for more details.", + "403" : { + "description" : "Access is forbidden!", "content" : { "application/json" : { "schema" : { @@ -250,10 +292,14 @@ } } }, - "200" : { - "description" : "Worker properties successfully replaced", + "500" : { + "description" : "The server were unable to process the request. See server-side logs for more details.", "content" : { - "application/json" : { } + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } } } } @@ -282,6 +328,9 @@ "required" : true }, "responses" : { + "201" : { + "description" : "Worker added successfully" + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -292,6 +341,16 @@ } } }, + "403" : { + "description" : "Access is forbidden!", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } + } + }, "409" : { "description" : "Worker already exists.", "content" : { @@ -311,9 +370,6 @@ } } } - }, - "201" : { - "description" : "Worker added successfully" } } }, @@ -330,6 +386,12 @@ } } ], "responses" : { + "200" : { + "description" : "Worker removed successfully", + "content" : { + "application/json" : { } + } + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -350,8 +412,8 @@ } } }, - "500" : { - "description" : "The server were unable to process the request. See server-side logs for more details.", + "403" : { + "description" : "Access is forbidden!", "content" : { "application/json" : { "schema" : { @@ -360,10 +422,14 @@ } } }, - "200" : { - "description" : "Worker removed successfully", + "500" : { + "description" : "The server were unable to process the request. See server-side logs for more details.", "content" : { - "application/json" : { } + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } } } } @@ -392,6 +458,12 @@ "required" : true }, "responses" : { + "200" : { + "description" : "Worker properties successfully updated", + "content" : { + "application/json" : { } + } + }, "400" : { "description" : "Bad request from the client", "content" : { @@ -402,8 +474,8 @@ } } }, - "500" : { - "description" : "The server were unable to process the request. See server-side logs for more details.", + "403" : { + "description" : "Access is forbidden!", "content" : { "application/json" : { "schema" : { @@ -412,10 +484,14 @@ } } }, - "200" : { - "description" : "Worker properties successfully updated", + "500" : { + "description" : "The server were unable to process the request. See server-side logs for more details.", "content" : { - "application/json" : { } + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorMessage" + } + } } } } diff --git a/signserver/doc/openapi.yaml b/signserver/doc/openapi.yaml index b698e6780..3056ba90e 100755 --- a/signserver/doc/openapi.yaml +++ b/signserver/doc/openapi.yaml @@ -19,12 +19,22 @@ paths: $ref: '#/components/schemas/WorkerRequest' required: true responses: + "201": + description: Worker added successfully + content: + application/json: {} "400": description: Bad request from the client content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "403": + description: Access is forbidden! + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "409": description: Worker already exists. content: @@ -38,10 +48,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "201": - description: Worker added successfully - content: - application/json: {} /workers/reload: post: summary: Reload workers @@ -54,12 +60,22 @@ paths: schema: $ref: '#/components/schemas/ReloadRequest' responses: + "200": + description: Workers successfully reloaded + content: + application/json: {} "400": description: Bad request from the client content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "403": + description: Access is forbidden! + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "404": description: No such worker content: @@ -103,14 +119,14 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "404": - description: No such worker + "403": + description: Access is forbidden! content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "503": - description: Crypto Token not available + "404": + description: No such worker content: application/json: schema: @@ -122,6 +138,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "503": + description: Crypto Token not available + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "200": description: The response data content: @@ -148,15 +170,18 @@ paths: $ref: '#/components/schemas/WorkerRequest' required: true responses: + "200": + description: Properties replaced successfully + content: + application/json: {} "400": description: Bad request from the client content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "500": - description: The server were unable to process the request. See server-side - logs for more details. + "403": + description: Access is forbidden! content: application/json: schema: @@ -167,10 +192,13 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "200": - description: Worker properties successfully replaced + "500": + description: The server were unable to process the request. See server-side + logs for more details. content: - application/json: {} + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' post: summary: Submit data for adding a new worker from multiple properties description: Submit a worker ID and a list of worker properties to add a new @@ -190,12 +218,20 @@ paths: $ref: '#/components/schemas/WorkerRequest' required: true responses: + "201": + description: Worker added successfully "400": description: Bad request from the client content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "403": + description: Access is forbidden! + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "409": description: Worker already exists. content: @@ -209,8 +245,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "201": - description: Worker added successfully delete: summary: Removing worker description: Removing worker by ID. @@ -222,6 +256,10 @@ paths: format: int32 type: integer responses: + "200": + description: Worker removed successfully + content: + application/json: {} "400": description: Bad request from the client content: @@ -234,6 +272,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "403": + description: Access is forbidden! + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "500": description: The server were unable to process the request. See server-side logs for more details. @@ -241,10 +285,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "200": - description: Worker removed successfully - content: - application/json: {} patch: summary: Submit data for update and delete worker properties description: Submit a worker ID and a list of worker properties to update or @@ -264,12 +304,22 @@ paths: $ref: '#/components/schemas/WorkerRequest' required: true responses: + "200": + description: Worker properties successfully updated + content: + application/json: {} "400": description: Bad request from the client content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' + "403": + description: Access is forbidden! + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' "500": description: The server were unable to process the request. See server-side logs for more details. @@ -277,10 +327,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorMessage' - "200": - description: Worker properties successfully updated - content: - application/json: {} components: schemas: DataEncoding: diff --git a/signserver/modules/SignServer-Client-CLI/src/main/java/org/signserver/client/cli/defaultimpl/RESTDocumentSigner.java b/signserver/modules/SignServer-Client-CLI/src/main/java/org/signserver/client/cli/defaultimpl/RESTDocumentSigner.java index 53fbccf47..b71155957 100644 --- a/signserver/modules/SignServer-Client-CLI/src/main/java/org/signserver/client/cli/defaultimpl/RESTDocumentSigner.java +++ b/signserver/modules/SignServer-Client-CLI/src/main/java/org/signserver/client/cli/defaultimpl/RESTDocumentSigner.java @@ -108,6 +108,7 @@ protected void sendRequest(final URL processServlet, conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("X-Keyfactor-Requested-With", "signclient"); conn.setDoOutput(true); conn.setAllowUserInteraction(false); diff --git a/signserver/modules/SignServer-Common/src/main/java/org/signserver/common/ForbiddenException.java b/signserver/modules/SignServer-Common/src/main/java/org/signserver/common/ForbiddenException.java new file mode 100644 index 000000000..cdeba8809 --- /dev/null +++ b/signserver/modules/SignServer-Common/src/main/java/org/signserver/common/ForbiddenException.java @@ -0,0 +1,26 @@ +/************************************************************************* + * * + * SignServer: The OpenSource Automated Signing Server * + * * + * This software is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or any later version. * + * * + * See terms of license at gnu.org. * + * * + *************************************************************************/ +package org.signserver.common; + +/** + * Exception where access is forbidden. + * + * @author Nima Saboonchi + */ +public class ForbiddenException extends IllegalRequestException { + + public ForbiddenException() { + super("Access is forbidden!"); + } + +} diff --git a/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/exception/ForbiddenExceptionMapper.java b/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/exception/ForbiddenExceptionMapper.java new file mode 100644 index 000000000..db9feb6da --- /dev/null +++ b/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/exception/ForbiddenExceptionMapper.java @@ -0,0 +1,35 @@ +/************************************************************************* + * * + * SignServer: The OpenSource Automated Signing Server * + * * + * This software is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or any later version. * + * * + * See terms of license at gnu.org. * + * * + *************************************************************************/ +package org.signserver.rest.api.exception; + +import org.signserver.common.ForbiddenException; +import org.signserver.rest.api.entities.ErrorMessage; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import static javax.ws.rs.core.Response.status; + +@Provider +public class ForbiddenExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ForbiddenException e) { + return status(Response.Status.FORBIDDEN) + .header("Content-Type", "application/json") + .entity(new ErrorMessage(e.getMessage())) + .build(); + } + +} \ No newline at end of file diff --git a/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/resource/WorkerResource.java b/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/resource/WorkerResource.java index 6e8584ca2..7bb774a5e 100644 --- a/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/resource/WorkerResource.java +++ b/signserver/modules/SignServer-REST-web/src/main/java/org/signserver/rest/api/resource/WorkerResource.java @@ -18,6 +18,7 @@ import org.bouncycastle.util.encoders.Base64; import org.signserver.admin.common.auth.AdminAuthHelper; import org.signserver.common.*; +import org.signserver.common.ForbiddenException; import org.signserver.common.data.Request; import org.signserver.common.data.SignatureRequest; import org.signserver.common.data.SignatureResponse; @@ -102,7 +103,6 @@ public class WorkerResource { private WorkerAuthHelper auth; - @PostConstruct protected void init() { dataFactory = DataUtils.createDataFactory(); @@ -117,11 +117,17 @@ protected void init() { * @param request Request data * @return The operation result in a JSON format. * @throws WorkerExistsException In case the given new worker ID already exists. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @POST @Path("{id}") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "201", + description = "Worker added successfully" + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -130,6 +136,14 @@ protected void init() { schema = @Schema(implementation = ErrorMessage.class) ) ) + @APIResponse( + responseCode = "403", + description = "Access is forbidden!", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) + ) @APIResponse( responseCode = "409", description = "Worker already exists.", @@ -146,10 +160,6 @@ protected void init() { schema = @Schema(implementation = ErrorMessage.class) ) ) - @APIResponse( - responseCode = "201", - description = "Worker added successfully" - ) @Operation( summary = "Submit data for adding a new worker from multiple properties", description = "Submit a worker ID and a list of worker properties to " @@ -163,6 +173,7 @@ public Response addWorker( description = "The request", required = true ) final WorkerRequest request) throws IllegalRequestException, AdminNotAuthorizedException { + checkCustomHeader(httpServletRequest); final Map tempProperties = request.getProperties(); if (tempProperties == null) { LOG.error("Properties in the request is not valid!"); @@ -192,11 +203,19 @@ public Response addWorker( * @param request Request data * @return The operation result in a JSON format. * @throws WorkerExistsException In case the given new worker ID already exists. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @POST @Path("/") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "201", + description = "Worker added successfully", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -205,6 +224,14 @@ public Response addWorker( schema = @Schema(implementation = ErrorMessage.class) ) ) + @APIResponse( + responseCode = "403", + description = "Access is forbidden!", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) + ) @APIResponse( responseCode = "409", description = "Worker already exists.", @@ -221,12 +248,6 @@ public Response addWorker( schema = @Schema(implementation = ErrorMessage.class) ) ) - @APIResponse( - responseCode = "201", - description = "Worker added successfully", - content = @Content( - mediaType = MediaType.APPLICATION_JSON) - ) @Operation( summary = "Submit data for adding a new worker from multiple properties", description = "Submit a worker ID and a list of worker properties to " @@ -239,7 +260,7 @@ public Response addWorkerWithoutID( description = "The request", required = true ) final WorkerRequest request) throws IllegalRequestException, AdminNotAuthorizedException { - + checkCustomHeader(httpServletRequest); Map tempProperties = request.getProperties(); if (tempProperties == null) { LOG.error("Properties in the request is not valid!"); @@ -273,11 +294,18 @@ public Response addWorkerWithoutID( * @return The operation result in a JSON format. * @throws NoSuchWorkerException In case the given worker ID not exists. * @throws AdminNotAuthorizedException If the admin is not authorized + * @throws ForbiddenException In case access is forbidden for the request. */ @PATCH @Path("{id}") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "200", + description = "Worker properties successfully updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -287,18 +315,20 @@ public Response addWorkerWithoutID( ) ) @APIResponse( - responseCode = "500", - description = "The server were unable to process the request. See server-side logs for more details.", + responseCode = "403", + description = "Access is forbidden!", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ErrorMessage.class) ) ) @APIResponse( - responseCode = "200", - description = "Worker properties successfully updated", + responseCode = "500", + description = "The server were unable to process the request. See server-side logs for more details.", content = @Content( - mediaType = MediaType.APPLICATION_JSON) + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) ) @Operation( summary = "Submit data for update and delete worker properties", @@ -312,7 +342,7 @@ public Response updateAndDeleteWorkerProperties( description = "The request", required = true ) final WorkerRequest request) throws IllegalRequestException, AdminNotAuthorizedException { - + checkCustomHeader(httpServletRequest); Map properties = request.getProperties(); if (properties == null) { LOG.error("Properties in the request is not valid!"); @@ -350,11 +380,19 @@ public Response updateAndDeleteWorkerProperties( * @param request Request data * @return The operation result in a JSON format. * @throws NoSuchWorkerException In case the given worker ID not exists. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @PUT @Path("{id}") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "200", + description = "Properties replaced successfully", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -364,8 +402,8 @@ public Response updateAndDeleteWorkerProperties( ) ) @APIResponse( - responseCode = "500", - description = "The server were unable to process the request. See server-side logs for more details.", + responseCode = "403", + description = "Access is forbidden!", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ErrorMessage.class) @@ -380,10 +418,12 @@ public Response updateAndDeleteWorkerProperties( ) ) @APIResponse( - responseCode = "200", - description = "Worker properties successfully replaced", + responseCode = "500", + description = "The server were unable to process the request. See server-side logs for more details.", content = @Content( - mediaType = MediaType.APPLICATION_JSON) + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) ) @Operation( summary = "Submit data for replace worker properties with the new properties", @@ -398,7 +438,7 @@ public Response replaceAllWorkerProperties( description = "The request", required = true ) final WorkerRequest request) throws IllegalRequestException, AdminNotAuthorizedException { - + checkCustomHeader(httpServletRequest); final Map properties = request.getProperties(); if (properties == null) { LOG.error("Properties in the request is not valid!"); @@ -418,10 +458,18 @@ public Response replaceAllWorkerProperties( * @param httpServletRequest Http Servlet request to extract request context from it * @return The operation result in a JSON format. * @throws NoSuchWorkerException In case the given worker ID not exists. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @DELETE @Path("{id}") @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "200", + description = "Worker removed successfully", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -439,8 +487,8 @@ public Response replaceAllWorkerProperties( ) ) @APIResponse( - responseCode = "404", - description = "No such worker", + responseCode = "403", + description = "Access is forbidden!", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ErrorMessage.class) @@ -454,12 +502,6 @@ public Response replaceAllWorkerProperties( schema = @Schema(implementation = ErrorMessage.class) ) ) - @APIResponse( - responseCode = "200", - description = "Worker removed successfully", - content = @Content( - mediaType = MediaType.APPLICATION_JSON) - ) @Operation( summary = "Removing worker", description = "Removing worker by ID." @@ -467,7 +509,8 @@ public Response replaceAllWorkerProperties( public Response removeWorker( @Context final HttpServletRequest httpServletRequest, @PathParam("id") final int id - ) throws NoSuchWorkerException, AdminNotAuthorizedException { + ) throws IllegalRequestException, AdminNotAuthorizedException { + checkCustomHeader(httpServletRequest); final AdminInfo adminInfo = auth.requireAdminAuthorization(getCertificate(httpServletRequest), "removeWorker", String.valueOf(id)); workerSession.removeWorker(adminInfo, id); @@ -482,11 +525,19 @@ public Response removeWorker( * @return The operation result in a JSON format. * @throws InternalServerException In case the request could not be processed by some error at the server side. * @throws NoSuchWorkerException In case any of the given worker IDs do not exist. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @POST @Path("reload") @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "200", + description = "Workers successfully reloaded", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -495,6 +546,14 @@ public Response removeWorker( schema = @Schema(implementation = ErrorMessage.class) ) ) + @APIResponse( + responseCode = "403", + description = "Access is forbidden!", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) + ) @APIResponse( responseCode = "404", description = "No such worker", @@ -521,6 +580,7 @@ public Response reload( @RequestBody( description = "The request" ) final ReloadRequest request) throws IllegalRequestException, AdminNotAuthorizedException { + checkCustomHeader(httpServletRequest); final List tempWorkerIDs = request.getWorkerIDs(); if (tempWorkerIDs == null || tempWorkerIDs.isEmpty()) { LOG.error("There is no Worker ID to reload!"); @@ -548,10 +608,26 @@ public Response reload( * REST operation for reload all workers. * * @return The operation result in a JSON format. + * @throws ForbiddenException In case access is forbidden for the request. + * @throws AdminNotAuthorizedException If the admin is not authorized */ @POST @Path("reload") @Produces({MediaType.APPLICATION_JSON}) + @APIResponse( + responseCode = "200", + description = "Workers successfully reloaded", + content = @Content( + mediaType = MediaType.APPLICATION_JSON) + ) + @APIResponse( + responseCode = "403", + description = "Access is forbidden!", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) + ) @APIResponse( responseCode = "500", description = "The server were unable to process the request. See server-side logs for more details.", @@ -561,7 +637,8 @@ public Response reload( ) ) public Response reloadAll( - @Context final HttpServletRequest httpServletRequest) throws AdminNotAuthorizedException { + @Context final HttpServletRequest httpServletRequest) throws AdminNotAuthorizedException, IllegalRequestException { + checkCustomHeader(httpServletRequest); List allWorkerIDs = workerSession.getAllWorkers(); for (int workerID : allWorkerIDs) { final AdminInfo adminInfo = auth.requireAdminAuthorization(getCertificate(httpServletRequest), "reloadAll", @@ -581,11 +658,17 @@ public Response reloadAll( * @return The response data * @throws RequestFailedException In case the request could not be processed typically because some error in the request data. * @throws InternalServerException In case the request could not be processed by some error at the server side. + * @throws ForbiddenException In case access is forbidden for the request. */ @POST @Path("{idOrName}/process") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) + @APIResponseSchema( + value = ProcessResponse.class, + responseCode = "200", + responseDescription = "The response data" + ) @APIResponse( responseCode = "400", description = "Bad request from the client", @@ -595,16 +678,16 @@ public Response reloadAll( ) ) @APIResponse( - responseCode = "404", - description = "No such worker", + responseCode = "403", + description = "Access is forbidden!", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ErrorMessage.class) ) ) @APIResponse( - responseCode = "503", - description = "Crypto Token not available", + responseCode = "404", + description = "No such worker", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ErrorMessage.class) @@ -618,10 +701,13 @@ public Response reloadAll( schema = @Schema(implementation = ErrorMessage.class) ) ) - @APIResponseSchema( - value = ProcessResponse.class, - responseCode = "200", - responseDescription = "The response data" + @APIResponse( + responseCode = "503", + description = "Crypto Token not available", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ErrorMessage.class) + ) ) @Operation( summary = "Submit data for processing", @@ -865,4 +951,11 @@ private X509Certificate getCertificate(HttpServletRequest httpServletRequest) th } return certificates; } + + private void checkCustomHeader(HttpServletRequest httpServletRequest) throws ForbiddenException { + if (httpServletRequest.getHeader("X-Keyfactor-Requested-With") == null) { + LOG.error("Missing required hedear X-Keyfactor-Requested-With"); + throw new ForbiddenException(); + } + } } diff --git a/signserver/modules/SignServer-Test-System/src/test/java/org/signserver/rest/RestWorkersTest.java b/signserver/modules/SignServer-Test-System/src/test/java/org/signserver/rest/RestWorkersTest.java index b9b74d8ff..ea6305c7a 100644 --- a/signserver/modules/SignServer-Test-System/src/test/java/org/signserver/rest/RestWorkersTest.java +++ b/signserver/modules/SignServer-Test-System/src/test/java/org/signserver/rest/RestWorkersTest.java @@ -67,6 +67,7 @@ public void testRestPostWorkersCMSSignerProcess() throws Exception { addSigner(CMSSigner.class.getName(), CMSSIGNER_WORKER_ID, CMSSIGNER_WORKER_NAME, true); // Check statusCode is 200 and response content type is json Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -98,6 +99,7 @@ public void testRestPostWorkersPlainSignerProcess() throws Exception { addSigner(PlainSigner.class.getName(), PLAINSIGNER_WORKER_ID, PLAINSIGNER_WORKER_NAME, true); // Check statusCode is 200 and response content type is json Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -130,6 +132,7 @@ public void testRestPostWorkersPDFSignerProcess() throws Exception { addSigner(PDFSigner.class.getName(), PDFSIGNER_WORKER_ID, PDFSIGNER_WORKER_NAME, true); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostRequestJsonBodyPDF()) @@ -173,6 +176,7 @@ public void testRestPostWorkersPlainSignerProcessWithoutMetadata() throws Except // Check statusCode is 200 and response content type is json Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -211,6 +215,7 @@ public void testRestPostWorkersPlainSignerProcessWithoutData() throws Exception // Check statusCode is 200 and response content type is json Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -235,6 +240,7 @@ public void testRestPostWorkersPlainSignerProcessWithoutData() throws Exception public void testRestNoSuchWorkerExceptionStatusCode() { LOG.debug("testRestNoSuchWorkerExceptionStatusCode"); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -260,6 +266,7 @@ public void testRestCryptoTokenOfflineExceptionStatusCode() throws Exception { try { addSigner(CMSSigner.class.getName(), CMSSIGNER_WORKER_ID, CMSSIGNER_WORKER_NAME, false); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -289,6 +296,7 @@ public void testRestIllegalRequestExceptionStatusCode() throws Exception { addSigner(CMSSigner.class.getName(), CMSSIGNER_WORKER_ID, CMSSIGNER_WORKER_NAME, true); JSONObject body = new JSONObject(); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -319,6 +327,7 @@ public void testInternalServerExceptionStatusCode() throws Exception { getWorkerSession().removeWorkerProperty(CMSSIGNER_WORKER_ID, "IMPLEMENTATION_CLASS"); getWorkerSession().reloadConfiguration(CMSSIGNER_WORKER_ID); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -349,6 +358,7 @@ public void testRestRequestFailedExceptionStatusCode() throws Exception { getWorkerSession().setWorkerProperty(CMSSIGNER_WORKER_ID, "ACCEPT_USERNAMES", "nonuser"); getWorkerSession().reloadConfiguration(CMSSIGNER_WORKER_ID); Response response = given() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostProcessRequestJsonBody()) @@ -368,6 +378,29 @@ public void testRestRequestFailedExceptionStatusCode() throws Exception { } } + /** + * Test REST POST without header. Should return status code 403 + */ + @Test + public void testAccessForbiddenStatusCode() { + LOG.debug("testAccessForbiddenStatusCode"); + Response response = given() + .contentType(JSON) + .accept(JSON) + .body(rtu.createPostWorkerAddRequestJsonBody(CMSSIGNER_WORKER_NAME)) + .when() + .post(baseURL + "/workers/" + CMSSIGNER_WORKER_ID) + .then() + .statusCode(403) + .contentType("application/json") + .extract().response(); + + JSONObject responseJsonObject = new JSONObject(response.jsonPath().getJsonObject("$")); + + assertEquals("Check response status code 403.", 403, response.statusCode()); + assertTrue("Check that the response contains error key.", responseJsonObject.containsKey("error")); + } + /** * Test REST POST to create a worker with provided properties and worker ID. */ @@ -378,6 +411,7 @@ public void testRestPostAddWorkerWithID() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -405,6 +439,7 @@ public void testRestPostAddWorkerIllegalRequestExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -433,6 +468,7 @@ public void testRestPostWorkerExistsExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -445,6 +481,7 @@ public void testRestPostWorkerExistsExceptionStatusCode() { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -475,6 +512,7 @@ public void testRestPostAddWorkerInternalServerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(dummyMessageBody) @@ -484,7 +522,6 @@ public void testRestPostAddWorkerInternalServerExceptionStatusCode() { .statusCode(500) .extract().response(); - JSONObject responseJsonObject = new JSONObject(response.jsonPath().getJsonObject("$")); assertEquals("Check response status code is 500.", 500, response.statusCode()); assertTrue("Check that the response contains error key.", responseJsonObject.containsKey("error")); @@ -504,6 +541,7 @@ public void testRestPostAddWorkerWithoutID() throws InvalidWorkerIdException { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -531,6 +569,7 @@ public void testRestPatchWorker() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -546,6 +585,7 @@ public void testRestPatchWorker() { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPatchWorkerEditRequestJsonBody()) @@ -576,6 +616,7 @@ public void testRestPatchWorkerIllegalRequestExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -589,6 +630,7 @@ public void testRestPatchWorkerIllegalRequestExceptionStatusCode() { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -618,6 +660,7 @@ public void testRestPatchWorkerNoSuchWorkerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPutWorkerReplaceRequestJsonBody(HELLO_WORKER_NAME)) @@ -647,6 +690,7 @@ public void testRestPatchWorkerInternalServerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(dummyMessageBody) @@ -676,6 +720,7 @@ public void testRestPutWorker() { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -693,6 +738,7 @@ public void testRestPutWorker() { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPutWorkerReplaceRequestJsonBody(HELLO_WORKER_NAME)) @@ -724,6 +770,7 @@ public void testRestPutWorkerIllegalRequestExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -737,6 +784,7 @@ public void testRestPutWorkerIllegalRequestExceptionStatusCode() { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(body) @@ -766,6 +814,7 @@ public void testRestPutWorkerNoSuchWorkerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPutWorkerReplaceRequestJsonBody(HELLO_WORKER_NAME)) @@ -795,6 +844,7 @@ public void testRestPutWorkerInternalServerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(dummyMessageBody) @@ -823,6 +873,7 @@ public void testRestDeleteWorker() throws InvalidWorkerIdException { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .body(rtu.createPostWorkerAddRequestJsonBody(HELLO_WORKER_NAME)) @@ -837,6 +888,7 @@ public void testRestDeleteWorker() throws InvalidWorkerIdException { response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .accept(JSON) .when() .delete(baseHttpsURL + "/workers/" + HELLO_WORKER_ID) @@ -847,7 +899,7 @@ public void testRestDeleteWorker() throws InvalidWorkerIdException { assertFalse("Check worker with the given worker name removed", getWorkerSession().getAllWorkers().contains(HELLO_WORKER_ID)); JSONObject responseJsonObject = new JSONObject(response.jsonPath().getJsonObject("$")); - assertTrue("Response contains the correct message", responseJsonObject.toString().contains("Worker removed successfully!")); + assertTrue("Response contains the correct message", responseJsonObject.toString().contains("Worker removed successfully")); assertEquals("Check response status code 200", 200, response.statusCode()); } finally { removeWorker(HELLO_WORKER_ID); @@ -865,6 +917,7 @@ public void testRestDeleteWorkerNoSuchWorkerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .when() @@ -893,6 +946,7 @@ public void testRestDeleteWorkerInternalServerExceptionStatusCode() { try { Response response = given() .relaxedHTTPSValidation() + .header("X-Keyfactor-Requested-With", "1") .contentType(JSON) .accept(JSON) .when()