From df807344e750464afc05f345f8f5ec98f8f4a39e Mon Sep 17 00:00:00 2001 From: AngeloCaporaso Date: Thu, 4 Apr 2024 15:46:20 +0200 Subject: [PATCH 1/2] chore: fix vulnerability --- .../gov/pagopa/gpd/upload/service/BlobService.java | 6 ++++++ .../upload/controller/FileUploadControllerTest.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java index 8e2cc2c..bfb9f46 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java @@ -158,6 +158,12 @@ private File unzip(CompletedFileUpload file) { // Disable auto-execution for extracted files outputFile = new File(sanitizedOutputPath); + + if (!outputFile.toPath().normalize().startsWith(destinationDirectory)) { + log.error("[Error][BlobService@unzip] Bad zip entry: the normalized file path does not start with the destination directory"); + throw new AppException(HttpStatus.BAD_REQUEST, "INVALID FILE", "Bad zip entry"); + } + boolean executableOff = outputFile.setExecutable(false); if(!executableOff) { log.error("[Error][BlobService@unzip] The underlying file system does not implement an execution permission and the operation failed."); diff --git a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java index fd5563d..7275bd6 100644 --- a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java +++ b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java @@ -20,8 +20,12 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; import static io.micronaut.http.HttpStatus.*; +import static java.nio.file.attribute.PosixFilePermission.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; @@ -39,7 +43,13 @@ class FileUploadControllerTest { @Test void createDebtPositionsByFile_OK() throws IOException { - File file = File.createTempFile("test", ".zip"); + // Creating a temporary file with a non-randomly generated name + File file = new File(System.getProperty("java.io.tmpdir"), "/test.zip"); + // Warning: This will fail on Windows as it doesn't support PosixFilePermissions. + Files.createFile( + file.toPath(), + PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_READ, OWNER_WRITE)) // permissions `-rw-------` + ); HttpRequest httpRequest = HttpRequest.create(HttpMethod.POST, URI) .contentType(MediaType.MULTIPART_FORM_DATA) From ea71c2b0d0059943de7059e6215f60a9ddb9a47a Mon Sep 17 00:00:00 2001 From: AngeloCaporaso Date: Thu, 4 Apr 2024 19:10:55 +0200 Subject: [PATCH 2/2] refactoring --- openapi/openapi.json | 1116 ++++++++--------- .../repository/BlobStorageRepository.java | 11 +- .../gpd/upload/service/BlobService.java | 31 +- .../controller/FileUploadControllerTest.java | 24 +- 4 files changed, 604 insertions(+), 578 deletions(-) diff --git a/openapi/openapi.json b/openapi/openapi.json index e912b17..5360993 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -1,641 +1,637 @@ { - "openapi": "3.0.1", - "info": { - "title": "pagopa-gpd-upload", - "version": "0.1.14" + "openapi" : "3.0.1", + "info" : { + "title" : "pagopa-gpd-upload", + "version" : "0.0.1" }, - "paths": { - "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file": { - "put": { - "tags": [ - "Debt Positions CRUD via file upload API" - ], - "summary": "The Organization updates the debt positions listed in the file.", - "operationId": "update-debt-positions-by-file-upload", - "parameters": [ - { - "name": "broker-code", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization-fiscal-code", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } + "paths" : { + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file" : { + "put" : { + "tags" : [ "Debt Positions CRUD via file upload API" ], + "summary" : "The Organization updates the debt positions listed in the file.", + "operationId" : "update-debt-positions-by-file-upload", + "parameters" : [ { + "name" : "broker-code", + "in" : "path", + "description" : "The broker code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + }, { + "name" : "organization-fiscal-code", + "in" : "path", + "description" : "The organization fiscal code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "File to be uploaded", - "format": "binary" + } ], + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "type" : "object", + "properties" : { + "file" : { + "type" : "string", + "description" : "File to be uploaded", + "format" : "binary" } } }, - "encoding": { - "file": { - "contentType": "application/octet-stream" + "encoding" : { + "file" : { + "contentType" : "application/octet-stream" } } } }, - "required": true + "required" : true }, - "responses": { - "202": { - "description": "Request accepted." + "responses" : { + "202" : { + "description" : "Request accepted." }, - "400": { - "description": "Malformed request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Malformed request.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Wrong or missing function key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "401" : { + "description" : "Wrong or missing function key.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "409": { - "description": "Conflict: duplicate file found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "409" : { + "description" : "Conflict: duplicate file found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } } - } + }, + "security" : [ { + "ApiKey" : [ ] + }, { + "Authorization" : [ ] + } ] }, - "post": { - "tags": [ - "Debt Positions CRUD via file upload API" - ], - "summary": "The Organization creates the debt positions listed in the file.", - "operationId": "create-debt-positions-by-file-upload", - "parameters": [ - { - "name": "broker-code", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization-fiscal-code", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } + "post" : { + "tags" : [ "Debt Positions CRUD via file upload API" ], + "summary" : "The Organization creates the debt positions listed in the file.", + "operationId" : "create-debt-positions-by-file-upload", + "parameters" : [ { + "name" : "broker-code", + "in" : "path", + "description" : "The broker code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + }, { + "name" : "organization-fiscal-code", + "in" : "path", + "description" : "The organization fiscal code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "File to be uploaded", - "format": "binary" + } ], + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "type" : "object", + "properties" : { + "file" : { + "type" : "string", + "description" : "File to be uploaded", + "format" : "binary" } } }, - "encoding": { - "file": { - "contentType": "application/octet-stream" + "encoding" : { + "file" : { + "contentType" : "application/octet-stream" } } } }, - "required": true + "required" : true }, - "responses": { - "202": { - "description": "Request accepted." + "responses" : { + "202" : { + "description" : "Request accepted." }, - "400": { - "description": "Malformed request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Malformed request.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Wrong or missing function key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "401" : { + "description" : "Wrong or missing function key.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "409": { - "description": "Conflict: duplicate file found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "409" : { + "description" : "Conflict: duplicate file found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } } - } + }, + "security" : [ { + "ApiKey" : [ ] + }, { + "Authorization" : [ ] + } ] }, - "delete": { - "tags": [ - "Debt Positions CRUD via file upload API" - ], - "summary": "The Organization deletes the debt positions based on IUPD listed in the file.", - "operationId": "delete-debt-positions-by-file-upload", - "parameters": [ - { - "name": "broker-code", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization-fiscal-code", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } + "delete" : { + "tags" : [ "Debt Positions CRUD via file upload API" ], + "summary" : "The Organization deletes the debt positions based on IUPD listed in the file.", + "operationId" : "delete-debt-positions-by-file-upload", + "parameters" : [ { + "name" : "broker-code", + "in" : "path", + "description" : "The broker code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "file": { - "type": "string", - "description": "File to be uploaded", - "format": "binary" + }, { + "name" : "organization-fiscal-code", + "in" : "path", + "description" : "The organization fiscal code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "type" : "object", + "properties" : { + "file" : { + "type" : "string", + "description" : "File to be uploaded", + "format" : "binary" } } }, - "encoding": { - "file": { - "contentType": "application/octet-stream" + "encoding" : { + "file" : { + "contentType" : "application/octet-stream" } } } }, - "required": true + "required" : true }, - "responses": { - "202": { - "description": "Request accepted." + "responses" : { + "202" : { + "description" : "Request accepted." }, - "400": { - "description": "Malformed request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Malformed request.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Wrong or missing function key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "401" : { + "description" : "Wrong or missing function key.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "409": { - "description": "Conflict: duplicate file found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "409" : { + "description" : "Conflict: duplicate file found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } } - } + }, + "security" : [ { + "ApiKey" : [ ] + }, { + "Authorization" : [ ] + } ] } }, - "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/report": { - "get": { - "tags": [ - "Upload Status API" - ], - "summary": "Returns the debt positions upload report.", - "operationId": "get-debt-positions-upload-report", - "parameters": [ - { - "name": "broker-code", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization-fiscal-code", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "file-id", - "in": "path", - "description": "The unique identifier for file upload", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/report" : { + "get" : { + "tags" : [ "Upload Status API" ], + "summary" : "Returns the debt positions upload report.", + "operationId" : "get-debt-positions-upload-report", + "parameters" : [ { + "name" : "broker-code", + "in" : "path", + "description" : "The broker code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + }, { + "name" : "organization-fiscal-code", + "in" : "path", + "description" : "The organization fiscal code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" } - ], - "responses": { - "200": { - "description": "Upload report found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadReport" + }, { + "name" : "file-id", + "in" : "path", + "description" : "The unique identifier for file upload", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "Upload report found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/UploadReport" } } } }, - "400": { - "description": "Malformed request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Malformed request.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Wrong or missing function key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "401" : { + "description" : "Wrong or missing function key.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "404": { - "description": "Upload report not found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "404" : { + "description" : "Upload report not found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } } - } + }, + "security" : [ { + "ApiKey" : [ ] + }, { + "Authorization" : [ ] + } ] } }, - "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/status": { - "get": { - "tags": [ - "Upload Status API" - ], - "summary": "Returns the debt positions upload status.", - "operationId": "get-debt-positions-upload-status", - "parameters": [ - { - "name": "broker-code", - "in": "path", - "description": "The broker code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "organization-fiscal-code", - "in": "path", - "description": "The organization fiscal code", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } - }, - { - "name": "file-id", - "in": "path", - "description": "The unique identifier for file upload", - "required": true, - "schema": { - "minLength": 1, - "type": "string" - } + "/brokers/{broker-code}/organizations/{organization-fiscal-code}/debtpositions/file/{file-id}/status" : { + "get" : { + "tags" : [ "Upload Status API" ], + "summary" : "Returns the debt positions upload status.", + "operationId" : "get-debt-positions-upload-status", + "parameters" : [ { + "name" : "broker-code", + "in" : "path", + "description" : "The broker code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + }, { + "name" : "organization-fiscal-code", + "in" : "path", + "description" : "The organization fiscal code", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" + } + }, { + "name" : "file-id", + "in" : "path", + "description" : "The unique identifier for file upload", + "required" : true, + "schema" : { + "minLength" : 1, + "type" : "string" } - ], - "responses": { - "200": { - "description": "Upload found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadStatus" + } ], + "responses" : { + "200" : { + "description" : "Upload found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/UploadStatus" } } } }, - "400": { - "description": "Malformed request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Malformed request.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Wrong or missing function key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "401" : { + "description" : "Wrong or missing function key.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "404": { - "description": "Upload not found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "404" : { + "description" : "Upload not found.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } } - } + }, + "security" : [ { + "ApiKey" : [ ] + }, { + "Authorization" : [ ] + } ] } }, - "/info": { - "get": { - "tags": [ - "Health check" - ], - "summary": "health check", - "description": "Return OK if application is started", - "operationId": "healthCheck", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AppInfo" + "/info" : { + "get" : { + "tags" : [ "Health check" ], + "summary" : "health check", + "description" : "Return OK if application is started", + "operationId" : "healthCheck", + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AppInfo" } } } }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "400" : { + "description" : "Bad Request", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "401" : { + "description" : "Unauthorized", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "allOf": [], - "anyOf": [], - "oneOf": [] + "403" : { + "description" : "Forbidden", + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ ], + "anyOf" : [ ], + "oneOf" : [ ] } } } }, - "429": { - "description": "Too many requests.", - "content": { - "text/json": {} + "429" : { + "description" : "Too many requests.", + "content" : { + "text/json" : { } } }, - "500": { - "description": "Service unavailable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemJson" + "500" : { + "description" : "Service unavailable", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ProblemJson" } } } @@ -644,123 +640,123 @@ } } }, - "components": { - "schemas": { - "AppInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "environment": { - "type": "string" + "components" : { + "schemas" : { + "AppInfo" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "version" : { + "type" : "string" + }, + "environment" : { + "type" : "string" } } }, - "ProblemJson": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable" - }, - "status": { - "type": "integer", - "description": "The HTTP status code generated by the origin server for this occurrence of the problem.", - "format": "int32", - "example": 200 - }, - "detail": { - "type": "string", - "description": "A human readable explanation specific to this occurrence of the problem.", - "example": "There was an error processing the request" + "ProblemJson" : { + "type" : "object", + "properties" : { + "title" : { + "type" : "string", + "description" : "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable" + }, + "status" : { + "type" : "integer", + "description" : "The HTTP status code generated by the origin server for this occurrence of the problem.", + "format" : "int32", + "example" : 200 + }, + "detail" : { + "type" : "string", + "description" : "A human readable explanation specific to this occurrence of the problem.", + "example" : "There was an error processing the request" } }, - "description": "Object returned as response in case of an error." + "description" : "Object returned as response in case of an error." }, - "ResponseEntry": { - "type": "object", - "properties": { - "statusCode": { - "type": "integer", - "format": "int32", - "example": 400 - }, - "statusMessage": { - "type": "string", - "example": "Bad request caused by invalid email address" - }, - "requestIDs": { - "type": "array", - "items": { - "type": "string" + "ResponseEntry" : { + "type" : "object", + "properties" : { + "statusCode" : { + "type" : "integer", + "format" : "int32", + "example" : 400 + }, + "statusMessage" : { + "type" : "string", + "example" : "Bad request caused by invalid email address" + }, + "requestIDs" : { + "type" : "array", + "items" : { + "type" : "string" } } } }, - "UploadReport": { - "type": "object", - "properties": { - "uploadID": { - "type": "string" - }, - "processedItem": { - "type": "integer", - "format": "int32" - }, - "submittedItem": { - "type": "integer", - "format": "int32" - }, - "responses": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResponseEntry" - } - }, - "startTime": { - "type": "string", - "format": "date-time", - "example": "2024-10-08T14:55:16.302Z" - }, - "endTime": { - "type": "string", - "format": "date-time", - "example": "2024-10-08T14:55:16.302Z" + "UploadReport" : { + "type" : "object", + "properties" : { + "uploadID" : { + "type" : "string" + }, + "processedItem" : { + "type" : "integer", + "format" : "int32" + }, + "submittedItem" : { + "type" : "integer", + "format" : "int32" + }, + "responses" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/ResponseEntry" + } + }, + "startTime" : { + "type" : "string", + "format" : "date-time", + "example" : "2024-10-08T14:55:16.302Z" + }, + "endTime" : { + "type" : "string", + "format" : "date-time", + "example" : "2024-10-08T14:55:16.302Z" } } }, - "UploadStatus": { - "type": "object", - "properties": { - "uploadID": { - "type": "string" - }, - "processedItem": { - "type": "integer", - "format": "int32" - }, - "submittedItem": { - "type": "integer", - "format": "int32" - }, - "startTime": { - "type": "string", - "format": "date-time", - "example": "2024-10-08T14:55:16.302Z" + "UploadStatus" : { + "type" : "object", + "properties" : { + "uploadID" : { + "type" : "string" + }, + "processedItem" : { + "type" : "integer", + "format" : "int32" + }, + "submittedItem" : { + "type" : "integer", + "format" : "int32" + }, + "startTime" : { + "type" : "string", + "format" : "date-time", + "example" : "2024-10-08T14:55:16.302Z" } } } }, - "securitySchemes": { - "Ocp-Apim-Subscription-Key": { - "type": "apiKey", - "name": "Ocp-Apim-Subscription-Key", - "in": "header" + "securitySchemes" : { + "Ocp-Apim-Subscription-Key" : { + "type" : "apiKey", + "name" : "Ocp-Apim-Subscription-Key", + "in" : "header" } } } -} +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java b/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java index 9eb48ed..42ad05a 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/repository/BlobStorageRepository.java @@ -70,6 +70,11 @@ private CompletableFuture uploadFileAsync(BlockBlobClient blockBlobClien return CompletableFuture.supplyAsync(() -> { try { String blobName = this.uploadFileBlocksAsBlockBlob(blockBlobClient, file); + + if(!file.delete()) { + log.error(String.format("[Error][BlobStorageRepository@uploadFileAsync] The file %s was not deleted", file.getName())); + } + return blobName; } catch (IOException e) { throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Error uploading file asynchronously", e); @@ -84,15 +89,15 @@ private String createRandomName(String namePrefix) { private String uploadFileBlocksAsBlockBlob(BlockBlobClient blockBlob, File file) throws IOException { InputStream inputStream = new FileInputStream(file); ByteArrayInputStream byteInputStream = null; - byte[] bytes = null; int blockSize = 1024 * 1024; + try { // Split the file into 1 MB blocks (block size deliberately kept small for the demo) and upload all the blocks int blockNum = 0; String blockId = null; String blockIdEncoded = null; - ArrayList blockList = new ArrayList(); - bytes = inputStream.readNBytes(blockSize); + ArrayList blockList = new ArrayList<>(); + byte[] bytes = inputStream.readNBytes(blockSize); while (bytes.length == blockSize) { byteInputStream = new ByteArrayInputStream(bytes); blockId = String.format("%05d", blockNum); // 5-digit number diff --git a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java index bfb9f46..7c9b600 100644 --- a/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java +++ b/src/main/java/it/gov/pagopa/gpd/upload/service/BlobService.java @@ -19,6 +19,7 @@ import java.io.*; import java.util.List; +import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -34,6 +35,7 @@ public class BlobService { private int zipMaxEntries; // Maximum number of entries allowed in the zip file private static final List ALLOWABLE_EXTENSIONS = List.of("json"); private static final List VALID_UPLOAD_EXTENSION = List.of("zip"); + private static final String DESTINATION_DIRECTORY = "upload-directory"; private ObjectMapper objectMapper; private final BlobStorageRepository blobStorageRepository; private final StatusService statusService; @@ -53,6 +55,9 @@ public BlobService(BlobStorageRepository blobStorageRepository, StatusService st public void init() { objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); + File directory = new File(DESTINATION_DIRECTORY); + if(!directory.exists()) + directory.mkdir(); } public String upsert(String broker, String organizationFiscalCode, UploadOperation uploadOperation, CompletedFileUpload fileUpload) { @@ -71,7 +76,14 @@ public String upsert(String broker, String organizationFiscalCode, UploadOperati .paymentPositions(paymentPositionsModel.getPaymentPositions()) .build(); - return upload(uploadInput, file, broker, organizationFiscalCode, paymentPositionsModel.getPaymentPositions().size()); + String uploadKey = upload(uploadInput, file, broker, organizationFiscalCode, paymentPositionsModel.getPaymentPositions().size()); + + if(!file.delete()) { + log.error(String.format("[Error][BlobService@upsert] The file %s was not deleted", file.getName())); + } + + + return uploadKey; } catch (IOException e) { log.error("[Error][BlobService@upload] " + e.getMessage()); throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR", "Internal server error", e.getCause()); @@ -94,7 +106,13 @@ public String delete(String broker, String organizationFiscalCode, UploadOperati .paymentPositionIUPDs(multipleIUPDModel.getPaymentPositionIUPDs()) .build(); - return upload(uploadInput, file, broker, organizationFiscalCode, multipleIUPDModel.getPaymentPositionIUPDs().size()); + String uploadKey = upload(uploadInput, file, broker, organizationFiscalCode, multipleIUPDModel.getPaymentPositionIUPDs().size()); + + if(!file.delete()) { + log.error(String.format("[Error][BlobService@delete] The file %s was not deleted", file.getName())); + } + + return uploadKey; } catch (IOException e) { log.error("[Error][BlobService@upload] " + e.getMessage()); throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR", "Internal server error", e.getCause()); @@ -120,7 +138,6 @@ public String upload(UploadInput in, File file, String broker, String organizati } private File unzip(CompletedFileUpload file) { - String destinationDirectory = "./"; int zipFiles = 0; int zipSize = 0; File outputFile = null; @@ -154,12 +171,12 @@ private File unzip(CompletedFileUpload file) { // Sanitize file name to remove potentially malicious characters String sanitizedFileName = sanitizeFileName(fileName); - String sanitizedOutputPath = destinationDirectory + File.separator + sanitizedFileName; + String sanitizedOutputPath = DESTINATION_DIRECTORY + File.separator + sanitizedFileName; // Disable auto-execution for extracted files outputFile = new File(sanitizedOutputPath); - if (!outputFile.toPath().normalize().startsWith(destinationDirectory)) { + if (!outputFile.toPath().normalize().startsWith(DESTINATION_DIRECTORY)) { log.error("[Error][BlobService@unzip] Bad zip entry: the normalized file path does not start with the destination directory"); throw new AppException(HttpStatus.BAD_REQUEST, "INVALID FILE", "Bad zip entry"); } @@ -215,11 +232,13 @@ private static String sanitizeFileName(String fileName) { return ""; } - // Replace invalid characters with underscores + // Match a single character not present in the list below [^\w._-] and replace invalid characters with underscores fileName = fileName.replaceAll("[^\\w._-]", "_"); // Normalize file name to lower case fileName = fileName.toLowerCase(); + String uuid = UUID.randomUUID().toString().replace("-", "").substring(0,8); + fileName = fileName.replace(".", uuid + "."); return fileName; } diff --git a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java index 7275bd6..69c2c19 100644 --- a/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java +++ b/src/test/java/it/gov/pagopa/gpd/upload/controller/FileUploadControllerTest.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; @@ -43,13 +44,7 @@ class FileUploadControllerTest { @Test void createDebtPositionsByFile_OK() throws IOException { - // Creating a temporary file with a non-randomly generated name - File file = new File(System.getProperty("java.io.tmpdir"), "/test.zip"); - // Warning: This will fail on Windows as it doesn't support PosixFilePermissions. - Files.createFile( - file.toPath(), - PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_READ, OWNER_WRITE)) // permissions `-rw-------` - ); + File file = getTempFile(); HttpRequest httpRequest = HttpRequest.create(HttpMethod.POST, URI) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -60,11 +55,12 @@ void createDebtPositionsByFile_OK() throws IOException { assertNotNull(response); assertEquals(ACCEPTED, response.getStatus()); + file.delete(); } @Test void updateDebtPositionsByFile_OK() throws IOException { - File file = File.createTempFile("test", ".zip"); + File file = getTempFile(); HttpRequest httpRequest = HttpRequest.create(HttpMethod.PUT, URI) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -75,11 +71,12 @@ void updateDebtPositionsByFile_OK() throws IOException { assertNotNull(response); assertEquals(ACCEPTED, response.getStatus()); + file.delete(); } @Test void deleteDebtPositionsByFile_OK() throws IOException { - File file = File.createTempFile("test", ".zip"); + File file = getTempFile(); HttpRequest httpRequest = HttpRequest.create(HttpMethod.DELETE, URI) .contentType(MediaType.MULTIPART_FORM_DATA) @@ -90,6 +87,15 @@ void deleteDebtPositionsByFile_OK() throws IOException { assertNotNull(response); assertEquals(ACCEPTED, response.getStatus()); + file.delete(); + } + + File getTempFile() throws IOException { + // Warning: This will fail on Windows as it doesn't support PosixFilePermissions. + return Files.createTempFile( + Path.of("./"), "test", ".zip", + PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_READ, OWNER_WRITE)) // permissions `-rw-------` + ).toFile(); } @Test