diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 825a24b4..326d55b6 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1605,6 +1605,45 @@ paths: schema: $ref: "#/components/schemas/Workflow" + /process-data-file-download/{modified_process_model_identifier}/{process_instance_id}/{process_data_identifier}: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: The modified id of an existing process model + schema: + type: string + - name: process_instance_id + in: path + required: true + description: The unique id of an existing process instance. + schema: + type: integer + - name: process_data_identifier + in: path + required: true + description: The identifier of the process data. + schema: + type: string + - name: index + in: query + required: false + description: The optional index of the value if key's value is an array + schema: + type: integer + get: + operationId: spiffworkflow_backend.routes.process_api_blueprint.process_data_file_download + summary: Download the file referneced in the process data value. + tags: + - Data Objects + responses: + "200": + description: Fetch succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Workflow" + /send-event/{modified_process_model_identifier}/{process_instance_id}: parameters: - name: modified_process_model_identifier diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 0e9bd581..82263475 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1,7 +1,9 @@ """APIs for dealing with process groups, process models, and process instances.""" +import base64 import json from typing import Any from typing import Dict +from typing import Optional import flask.wrappers from flask import Blueprint @@ -81,10 +83,12 @@ def process_list() -> Any: return SpecReferenceSchema(many=True).dump(references) -def process_data_show( +def _process_data_fetcher( process_instance_id: int, process_data_identifier: str, modified_process_model_identifier: str, + download_file_data: bool, + index: Optional[int] = None, ) -> flask.wrappers.Response: """Process_data_show.""" process_instance = _find_process_instance_by_id_or_raise(process_instance_id) @@ -94,6 +98,26 @@ def process_data_show( if process_data_identifier in all_process_data: process_data_value = all_process_data[process_data_identifier] + if process_data_value is not None and index is not None: + process_data_value = process_data_value[index] + + if ( + download_file_data + and isinstance(process_data_value, str) + and process_data_value.startswith("data:") + ): + parts = process_data_value.split(";") + mimetype = parts[0][4:] + filename = parts[1] + base64_value = parts[2].split(",")[1] + file_contents = base64.b64decode(base64_value) + + return Response( + file_contents, + mimetype=mimetype, + headers={"Content-disposition": f"attachment; filename={filename}"}, + ) + return make_response( jsonify( { @@ -105,6 +129,37 @@ def process_data_show( ) +def process_data_show( + process_instance_id: int, + process_data_identifier: str, + modified_process_model_identifier: str, +) -> flask.wrappers.Response: + """Process_data_show.""" + return _process_data_fetcher( + process_instance_id, + process_data_identifier, + modified_process_model_identifier, + False, + None, + ) + + +def process_data_file_download( + process_instance_id: int, + process_data_identifier: str, + modified_process_model_identifier: str, + index: Optional[int] = None, +) -> flask.wrappers.Response: + """Process_data_file_download.""" + return _process_data_fetcher( + process_instance_id, + process_data_identifier, + modified_process_model_identifier, + True, + index, + ) + + # sample body: # {"ref": "refs/heads/main", "repository": {"name": "sample-process-models", # "full_name": "sartography/sample-process-models", "private": False .... }} diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py index 6873198a..6fd7d39c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/user.py @@ -17,6 +17,7 @@ from flask import request from werkzeug.wrappers import Response from spiffworkflow_backend.exceptions.api_error import ApiError +from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authentication_service import AuthenticationService from spiffworkflow_backend.services.authentication_service import ( @@ -58,6 +59,10 @@ def verify_token( if not token and "Authorization" in request.headers: token = request.headers["Authorization"].removeprefix("Bearer ") + if not token and "access_token" in request.cookies: + if request.path.startswith(f"{V1_API_PATH_PREFIX}/process-data-file-download/"): + token = request.cookies["access_token"] + # This should never be set here but just in case _clear_auth_tokens_from_thread_local_data() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/scripts/markdown_file_download_link.py b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/markdown_file_download_link.py new file mode 100644 index 00000000..3952525b --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/scripts/markdown_file_download_link.py @@ -0,0 +1,51 @@ +"""Markdown_file_download_link.""" +from typing import Any +from urllib.parse import unquote + +from flask import current_app + +from spiffworkflow_backend.models.process_model import ProcessModelInfo +from spiffworkflow_backend.models.script_attributes_context import ( + ScriptAttributesContext, +) +from spiffworkflow_backend.scripts.script import Script + + +class GetMarkdownFileDownloadLink(Script): + """GetMarkdownFileDownloadLink.""" + + @staticmethod + def requires_privileged_permissions() -> bool: + """We have deemed this function safe to run without elevated permissions.""" + return False + + def get_description(self) -> str: + """Get_description.""" + return """Returns a string which is a string in markdown format.""" + + def run( + self, + script_attributes_context: ScriptAttributesContext, + *_args: Any, + **kwargs: Any, + ) -> Any: + """Run.""" + # example input: + # "data:application/pdf;name=Harmeet_1234.pdf;base64,JV...." + process_data_identifier = kwargs["key"] + parts = kwargs["file_data"].split(";") + file_index = kwargs["file_index"] + label = unquote(parts[1].split("=")[1]) + process_model_identifier = script_attributes_context.process_model_identifier + modified_process_model_identifier = ( + ProcessModelInfo.modify_process_identifier_for_path_param( + process_model_identifier + ) + ) + process_instance_id = script_attributes_context.process_instance_id + url = current_app.config["SPIFFWORKFLOW_BACKEND_URL"] + url += f"/v1.0/process-data-file-download/{modified_process_model_identifier}/" + f"{process_instance_id}/{process_data_identifier}?index={file_index}" + link = f"[{label}]({url})" + + return link