diff --git a/.idea/avm-server.iml b/.idea/avm-server.iml index 85b3f41..8b88e2f 100644 --- a/.idea/avm-server.iml +++ b/.idea/avm-server.iml @@ -1,105 +1,8 @@ - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -107,78 +10,76 @@ diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..32ee99a --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + { + "associatedIndex": 3 +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1710496180200 + + + + + + + + + + + + \ No newline at end of file diff --git a/Gemfile b/Gemfile index 213eb24..61874a1 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ gem "base64" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri windows ] + gem "debug", ">= 1.6.2", platforms: %i[ mri windows ] gem 'dotenv-rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 05bbc15..9482519 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -220,7 +220,7 @@ PLATFORMS DEPENDENCIES base64 bootsnap - debug + debug (>= 1.6.2) dotenv-rails faraday jbuilder diff --git a/app/controllers/api/v1/documents_controller.rb b/app/controllers/api/v1/documents_controller.rb index ccf882d..d044d19 100644 --- a/app/controllers/api/v1/documents_controller.rb +++ b/app/controllers/api/v1/documents_controller.rb @@ -1,12 +1,4 @@ class Api::V1::DocumentsController < ApplicationController - rescue_from AvmServiceBadRequestError do |e| - render json: JSON.parse(e.message), status: 400 - end - - rescue_from AvmServiceInternalError do |e| - render json: JSON.parse(e.message), status: 500 - end - before_action :set_document, only: %i[ show datatosign sign visualization destroy ] before_action :set_key, only: %i[ show create datatosign sign visualization ] before_action :decrypt_document_content, only: %i[ show sign datatosign visualization destroy] @@ -33,6 +25,7 @@ def visualization # POST /documents/1/datatosign def datatosign + @document.set_add_timestamp if datatosign_params[:addTimestamp] @signing_certificate = datatosign_params.require(:signingCertificate) @result = @document.datatosign(@signing_certificate) end @@ -40,9 +33,10 @@ def datatosign # POST /documents/1/sign def sign @signer = @document.sign(@key, sign_params[:dataToSignStructure], sign_params[:signedData]) - unless @signer - render json: @document.errors, status: :unprocessable_entity - end + render json: @document.errors, status: :unprocessable_entity unless @signer + + @document = Document.find(params[:id]) + decrypt_document_content end # DELETE /documents/1 @@ -57,7 +51,13 @@ def set_document def set_key @key = request.headers.to_h['HTTP_X_ENCRYPTION_KEY'] || params[:encryptionKey] - raise ActionController::ParameterMissing.new('X-Encryption-Key') unless @key + raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MISSING", "Encryption key not provided.", "Encryption key must be provided either in X-Encryption-Key header or as encryptionKey query parameter.") unless @key + # TODO + # raise AvmUnauthorizedError.new("ENCRYPTION_KEY_MALFORMED", "Encryption key invalid.", "Encryption key must be a 64 character long hexadecimal string.") unless validate_key(@key) + end + + def validate_key(key) + key.length == 64 and !key[/\H/] end def decrypt_document_content @@ -74,7 +74,7 @@ def document_params end def datatosign_params - params.permit(:encryptionKey, :id, :signingCertificate) + params.permit(:encryptionKey, :id, :signingCertificate, :addTimestamp) end def sign_params diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e1aa106..8999038 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,15 +1,33 @@ class ApplicationController < ActionController::API + rescue_from ActionController::ParameterMissing do |e| + render json: { + code: "PARAMETER_MISSING", + message: "Required parameter is missing.", + details: e.message + }, status: 400 + end - class WrongEncryptionKey < ArgumentError + rescue_from AvmServiceBadRequestError do |e| + render json: JSON.parse(e.message), status: 422 end - rescue_from WrongEncryptionKey do |e| - render_forbidden + rescue_from AvmServiceInternalError do |e| + render json: JSON.parse(e.message), status: 502 end - private + rescue_from AvmUnauthorizedError do |e| + render json: { + code: e.code, + message: e.message, + details: e.details + }, status: 401 + end - def render_forbidden - render status: :forbidden, json: { message: "Forbidden" } + rescue_from AvmBadEncryptionKeyError do |e| + render json: { + code: "ENCRYPTION_KEY_MISMATCH", + message: "Encryption key mismatch.", + details: "Provided encryption key failed to decrypt document." + }, status: 403 end end diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 0000000..2b78858 --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,16 @@ +class ErrorsController < ApplicationController + def bad_request + render json: { + code: "BAD_REQUEST", + message: "Bad request" + },status: 400 + end + + def internal_error + render json: { + code: "INTERNAL_ERROR", + message: "Unexpected error happened.", + details: "If you are a maintainer of this server instance see logs for more information." + }, status: 500 + end +end \ No newline at end of file diff --git a/app/errors/avm_bad_encryption_key_error.rb b/app/errors/avm_bad_encryption_key_error.rb new file mode 100644 index 0000000..055d8aa --- /dev/null +++ b/app/errors/avm_bad_encryption_key_error.rb @@ -0,0 +1,9 @@ +class AvmBadEncryptionKeyError < StandardError + def initialize(body_str) + @message = body_str + end + + def message + @message + end +end diff --git a/app/errors/avm_unauthorized_error.rb b/app/errors/avm_unauthorized_error.rb new file mode 100644 index 0000000..6df817c --- /dev/null +++ b/app/errors/avm_unauthorized_error.rb @@ -0,0 +1,19 @@ +class AvmUnauthorizedError < StandardError + def initialize(code, message, details) + @code = code + @message = message + @details = details + end + + def code + @code + end + + def message + @message + end + + def details + @details + end +end diff --git a/app/models/document.rb b/app/models/document.rb index 473edea..fc658f9 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -37,6 +37,11 @@ def visualization avm_service.visualization(self) end + def set_add_timestamp + parameters['level'] = parameters['level'].gsub(/BASELINE_B/, 'BASELINE_T') + save! + end + def datatosign(signing_certificate) avm_service.datatosign(self, signing_certificate) end diff --git a/config/application.rb b/config/application.rb index 18d200a..67aed6e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,6 +34,8 @@ class Application < Rails::Application # Skip views, helpers and assets when generating a new resource. config.api_only = true + config.exceptions_app = self.routes + Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid } end end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index c7c070c..0000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -nCP/4zwawW6kjWhwysmAx1J0B5lhTLveNfHNFrJlvFabwdt59tatyaVbP/JDsS7z7knDh7kD53MRq/5IxjYYy+Op7o+XY381ZApiDxmJZ3avYTkh06KZUapx0JuFzNFL8OweLLjGZo678FK8h1Wadrp8hH4Ozcow3SqZCiadqykMeEgdRyKjmFFmtkWFWRLAYLGuN8PG8wJG8zYMdkYsgbK3I+RY0aMGZp5b3nH5lKsQDsQXTT46uQCZKJT0Oh7jeUCO4fW+Fwy3S9bEGPkEs5v5oqxvQHHcSZUvVEXAhaB/WAUB0mvPgcTkQY6qo72hEwmQPMKHr+oQm9Cfw3XhwWQ68r36/DTi879arm5ALvuedvoz1CRpLFyoORo2pwBQ+SRcn0cuZ9K+MTUZT6LxBoWS0rJG--da/ePvnZInyUUqRc--WssXdoWwMSXrlnlXusTGOQ== \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 517040a..1c93c38 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ Rails.application.routes.draw do + get "/400", to: "errors#bad_request" + get "/500", to: "errors#internal_error" + namespace :api do namespace :v1 do resources :documents, only: [:show, :create, :destroy] do diff --git a/public/openapi.yaml b/public/openapi.yaml index 429ce10..09c7f26 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -38,32 +38,12 @@ paths: required: true allowEmptyValue: false responses: - 400: - description: Bad request - content: - application/json: - schema: - type: object - properties: - status: - type: integer - error: - type: string - 401: - description: EncryptionKey not provided 200: description: Server stored the data and returned GUID so the client can further access the data. content: "application/json": schema: - type: object - properties: - guid: - type: string - description: GUID of the posted document - example: bfde97b4-ee27-47bc-97e2-5164ed96a92a - required: - - guid + $ref: "#/components/schemas/CreateDocumentResponseBody" headers: Last-Modified: schema: @@ -71,6 +51,36 @@ paths: pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$' example: 'Tue, 15 Oct 2019 12:45:26 GMT' description: Datetime of the last-modified attribute of the uploaded file. Useful for polling with the GET document request. + 400: + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" + 401: + description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" + 422: + description: Unprocessable content + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" /documents/{guid}: get: @@ -79,7 +89,7 @@ paths: description: | External system requests signed document at the end of the process. - This endpoint is also designed for polling with the `If-Modified-Since` header. + This endpoint is also designed for polling with the `If-Modified-Since` header (`TODO`). parameters: - name: guid in: path @@ -102,29 +112,21 @@ paths: required: true allowEmptyValue: false responses: - 400: - description: Bad request - 401: - description: EncryptionKey not provided - 403: - description: EncryptionKey mismatch - 404: - description: Not found - 304: - description: Requested document has not been modified since `If-Modified-Since` header - headers: - Last-Modified: - schema: - type: string - pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$' - example: 'Tue, 15 Oct 2019 12:45:26 GMT' - description: Datetime of the last-modified attribute of the requeste document. 200: description: Requested document with an array of its signers content: "application/json": schema: $ref: "#/components/schemas/GetDocumentResponse" + headers: + Last-Modified: + schema: + type: string + pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$' + example: 'Tue, 15 Oct 2019 12:45:26 GMT' + description: "`TODO` Datetime of the last-modified attribute of the requeste document." + 304: + description: Requested document has not been modified since `If-Modified-Since` header headers: Last-Modified: schema: @@ -132,6 +134,44 @@ paths: pattern: '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$' example: 'Tue, 15 Oct 2019 12:45:26 GMT' description: Datetime of the last-modified attribute of the requeste document. + 400: + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" + 401: + description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" + 403: + description: EncryptionKey mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyMismatchErrorResponseBody" + 404: + description: Not found + 422: + description: Unprocessable content + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" delete: description: | @@ -146,16 +186,46 @@ paths: required: true example: aedf97b4-ee27-47bc-97e2-5164ed96a92a responses: + 204: + description: Document deleted 400: description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" 401: description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" 403: description: EncryptionKey mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyMismatchErrorResponseBody" 404: description: Not found - 204: - description: Document deleted + 422: + description: Unprocessable content + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" /documents/{guid}/visualization: get: @@ -178,39 +248,50 @@ paths: required: true allowEmptyValue: false responses: + 200: + description: OK + content: + "application/json": + schema: + $ref: "#/components/schemas/VisualizationResponse" 400: description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" 401: description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" 403: description: EncryptionKey mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyMismatchErrorResponseBody" 404: description: Not found - 200: - description: OK + 422: + description: Unprocessable content content: - "application/json": + application/json: + schema: + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: schema: - type: object - properties: - mimeType: - type: string - enum: - - "application/pdf" - - "text/html" - - "text/plain" - example: "application/pdf" - filename: - type: string - description: Name of the document - example: sample_document.pdf - content: - type: string - description: Base64 encoded contentto be displayed - example: ZXhhbXBsZSBzdHJpbmcgaW4gYmFzZTY0Cg== - required: - - mimeType - - content + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" /documents/{guid}/datatosign: post: @@ -236,27 +317,52 @@ paths: content: "application/json": schema: - type: object - properties: - signingCertificate: - $ref: "#/components/schemas/SigningCertificate" - required: - - signingCertificate + $ref: "#/components/schemas/DataToSignRequestBody" responses: + 200: + description: Computed DataToSign and exact SigningTime. + content: + "application/json": + schema: + $ref: "#/components/schemas/DataToSignStructure" 400: description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" 401: description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" 403: description: EncryptionKey mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyMismatchErrorResponseBody" 404: description: Not found - 200: - description: Computed DataToSign and exact SigningTime. + 422: + description: Unprocessable content content: - "application/json": + application/json: schema: - $ref: "#/components/schemas/DataToSignStructure" + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" /documents/{guid}/sign: post: @@ -291,25 +397,53 @@ paths: schema: $ref: "#/components/schemas/SignRequestBody" responses: + 200: + description: | + Document was sucessfuly signed, can be obtained via GET signed request, and is also returned in the response. + + When `"returnSignedDocument": false` Document.Content is empty. + content: + "application/json": + schema: + $ref: "#/components/schemas/SignDocumentResponse" 400: description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestErrorResponseBody" 401: description: EncryptionKey not provided + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyNotProvidedErrorResponseBody" 403: description: EncryptionKey mismatch + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyMismatchErrorResponseBody" 404: description: Not found 422: - description: Unprocessable entity - possibly dataToSign mismatch. - 200: - description: | - Document was sucessfuly signed, can be obtained via GET signed request, and is also returned in the response. - - When `"returnSignedDocument": false` Document.Content is empty. + description: Unprocessable content content: - "application/json": + application/json: schema: - $ref: "#/components/schemas/SignDocumentResponse" + $ref: "#/components/schemas/ErrorResponseBody" + 500: + description: Internal error + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponseBody" + 502: + description: Bad gateway - internal error on AVM microservice + content: + application/json: + schema: + $ref: "#/components/schemas/BadGatewayErrorResponseBody" components: schemas: @@ -347,6 +481,19 @@ components: description: Epoch timestamp in milliseconds of the signing time example: 1707900119123 + DataToSignRequestBody: + type: object + properties: + signingCertificate: + $ref: "#/components/schemas/SigningCertificate" + addTimestamp: + type: boolean + description: Indication whether to add a timestamp to the signature if it was not set at document creation time + example: true + default: false + required: + - signingCertificate + DataToSignStructure: type: object properties: @@ -490,15 +637,160 @@ components: required: - level + CreateDocumentResponseBody: + type: object + properties: + guid: + type: string + description: GUID of the posted document + example: bfde97b4-ee27-47bc-97e2-5164ed96a92a + required: + - guid + + VisualizationResponse: + type: object + properties: + mimeType: + type: string + example: "application/pdf;base64" + filename: + type: string + description: Name of the document + example: sample_document.pdf + content: + type: string + description: Base64 encoded contentto be displayed + example: ZXhhbXBsZSBzdHJpbmcgaW4gYmFzZTY0Cg== + required: + - mimeType + - content + + BadRequestErrorResponseBody: + type: object + properties: + code: + type: string + example: BAD_REQUEST + description: Code that can be used to identify the error. + message: + type: string + example: Parameter missing. + description: Human readable error message. + details: + type: string + example: Document ID is missing in request. + description: Optional details. + required: + - code + - message + + ErrorResponseBody: + type: object + properties: + code: + type: string + example: UNPROCESSABLE_INPUT + description: Code that can be used to identify the error. + message: + type: string + example: IllegalArgumentException parsing request body + description: Human readable error message. + details: + type: string + example: Document must be a PDF when using PAdES. + description: Optional details. + required: + - code + - message + + EncryptionKeyNotProvidedErrorResponseBody: + type: object + properties: + code: + type: string + enum: + - ENCRYPTION_KEY_MISSING + - ENCRYPTION_KEY_MALFORMED + description: Code that can be used to identify the error. + message: + type: string + example: Encryption key not provided. + description: Human readable error message. + details: + type: string + example: Encryption key must be provided either in X-Encryption-Key header or as encryptionKey query parameter. + description: Optional details. + required: + - code + - message + + EncryptionKeyMismatchErrorResponseBody: + type: object + properties: + code: + type: string + enum: + - ENCRYPTION_KEY_MISMATCH + description: Code that can be used to identify the error. + message: + type: string + example: Encryption key mismatch. + description: Human readable error message. + details: + type: string + example: Provided encryption key failed to decrypt document. + description: Optional details. + required: + - code + - message + + InternalErrorResponseBody: + type: object + properties: + code: + type: string + example: INTERNAL_ERROR + description: Code that can be used to identify the error. + message: + type: string + example: Unexpected error while signing document. + description: Human readable error message. + details: + type: string + example: Something unexpected happened. + description: Optional details. + required: + - code + - message + + BadGatewayErrorResponseBody: + type: object + properties: + code: + type: string + example: UNRECOGNIZED_DSS_ERROR + description: Code that can be used to identify the error. + message: + type: string + example: Unexpected error while signing document. + description: Human readable error message. + details: + type: string + example: Something unexpected happened. + description: Optional details. + required: + - code + - message + securitySchemes: Header: type: apiKey in: header name: X-Encryption-Key - description: AES256 encryption key in hexadecimal form that is used to encrypt and decrypt signing doucment. + description: AES256 encryption key in hexadecimal form (64 characters) that is used to encrypt and decrypt signing doucment. Parameter: type: apiKey in: query name: encryptionKey - description: AES256 encryption key in hexadecimal form that is used to encrypt and decrypt signing doucment. + description: AES256 encryption key in hexadecimal form (64 characters) that is used to encrypt and decrypt signing doucment.