From 558c616744d580194ce60acdb06fd67c5476ace5 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 2 May 2023 16:31:29 -0400 Subject: [PATCH] disallow saving a process model file if it has changed w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 7 ++++ .../src/spiffworkflow_backend/models/file.py | 40 +++++-------------- .../models/process_model.py | 2 +- .../routes/process_models_controller.py | 29 +++++++++----- .../services/spec_file_service.py | 1 - spiffworkflow-frontend/src/interfaces.ts | 1 + .../src/routes/ProcessModelEditDiagram.tsx | 3 ++ 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 0ea5291ce..0bec6684f 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1245,6 +1245,13 @@ paths: put: operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update summary: save the contents to the given file + parameters: + - name: file_contents_hash + in: query + required: true + description: The hash of the file contents that originally came with the file. + schema: + type: string tags: - Process Model Files requestBody: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py index eb8d706db..5a29aec1c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py @@ -1,13 +1,10 @@ -"""File.""" +from __future__ import annotations from dataclasses import dataclass +from typing import Any from dataclasses import field from datetime import datetime from typing import Optional -import marshmallow -from marshmallow import INCLUDE -from marshmallow import Schema - from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum from spiffworkflow_backend.models.spec_reference import SpecReference @@ -77,7 +74,7 @@ class File: references: Optional[list[SpecReference]] = None file_contents: Optional[bytes] = None process_model_id: Optional[str] = None - process_group_id: Optional[str] = None + file_contents_hash: Optional[str] = None def __post_init__(self) -> None: """__post_init__.""" @@ -102,28 +99,9 @@ class File: ) return instance - -class FileSchema(Schema): - """FileSchema.""" - - class Meta: - """Meta.""" - - model = File - fields = [ - "id", - "name", - "content_type", - "last_modified", - "type", - "size", - "data_store", - "user_uid", - "url", - "file_contents", - "references", - "process_group_id", - "process_model_id", - ] - unknown = INCLUDE - references = marshmallow.fields.List(marshmallow.fields.Nested("SpecReferenceSchema")) + @property + def serialized(self) -> dict[str, Any]: + dictionary = self.__dict__ + if isinstance(self.file_contents, bytes): + dictionary['file_contents'] = self.file_contents.decode('utf-8') + return dictionary diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py index c1f57fbb6..a5a6a9240 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_model.py @@ -87,7 +87,7 @@ class ProcessModelInfoSchema(Schema): display_order = marshmallow.fields.Integer(allow_none=True) primary_file_name = marshmallow.fields.String(allow_none=True) primary_process_id = marshmallow.fields.String(allow_none=True) - files = marshmallow.fields.List(marshmallow.fields.Nested("FileSchema")) + files = marshmallow.fields.List(marshmallow.fields.Nested("File")) fault_or_suspend_on_exception = marshmallow.fields.String() exception_notification_addresses = marshmallow.fields.List(marshmallow.fields.String) metadata_extraction_paths = marshmallow.fields.List( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 880cabda7..84376eca2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -1,4 +1,5 @@ """APIs for dealing with process groups, process models, and process instances.""" +from hashlib import sha256 import json import os import re @@ -18,7 +19,6 @@ from werkzeug.datastructures import FileStorage from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.interfaces import IdToProcessGroupMapping -from spiffworkflow_backend.models.file import FileSchema from spiffworkflow_backend.models.process_group import ProcessGroup from spiffworkflow_backend.models.process_instance_report import ( ProcessInstanceReportModel, @@ -245,10 +245,9 @@ def process_model_list( return make_response(jsonify(response_json), 200) -def process_model_file_update(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response: - """Process_model_file_update.""" +def process_model_file_update(modified_process_model_identifier: str, file_name: str, file_contents_hash: str) -> flask.wrappers.Response: message = f"User: {g.user.username} clicked save for" - return _create_or_update_process_model_file(modified_process_model_identifier, message, 200) + return _create_or_update_process_model_file(modified_process_model_identifier, message, 200, file_contents_hash=file_contents_hash) def process_model_file_delete(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response: @@ -293,7 +292,6 @@ def process_model_file_create( def process_model_file_show(modified_process_model_identifier: str, file_name: str) -> Any: - """Process_model_file_show.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") process_model = _get_process_model(process_model_identifier) files = SpecFileService.get_files(process_model, file_name) @@ -309,8 +307,10 @@ def process_model_file_show(modified_process_model_identifier: str, file_name: s file = files[0] file_contents = SpecFileService.get_data(process_model, file.name) file.file_contents = file_contents + file_contents_hash = sha256(file_contents).hexdigest() + file.file_contents_hash = file_contents_hash file.process_model_id = process_model.id - return FileSchema().dump(file) + return make_response(jsonify(file), 200) # { @@ -477,6 +477,7 @@ def _create_or_update_process_model_file( modified_process_model_identifier: str, message_for_git_commit: str, http_status_to_return: int, + file_contents_hash: Optional[str], ) -> flask.wrappers.Response: """_create_or_update_process_model_file.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") @@ -498,6 +499,16 @@ def _create_or_update_process_model_file( status_code=400, ) + current_file_contents_bytes = SpecFileService.get_data(process_model, request_file.filename) + if current_file_contents_bytes and file_contents_hash: + current_file_contents_hash = sha256(current_file_contents_bytes).hexdigest() + if current_file_contents_hash != file_contents_hash: + raise ApiError( + error_code="process_model_file_has_changed", + message=f"Process model file: {request_file.filename} was already changed by someone else. Please reload before making changes.", + status_code=409, + ) + file = None try: file = SpecFileService.update_file(process_model, request_file.filename, request_file_contents) @@ -514,8 +525,4 @@ def _create_or_update_process_model_file( file.process_model_id = process_model.id _commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}") - return Response( - json.dumps(FileSchema().dump(file)), - status=http_status_to_return, - mimetype="application/json", - ) + return make_response(jsonify(file), http_status_to_return) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index 0b2f963a4..1f7d1e0f2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -175,7 +175,6 @@ class SpecFileService(FileSystemService): @classmethod def update_file(cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes) -> File: - """Update_file.""" SpecFileService.assert_valid_file_name(file_name) cls.validate_bpmn_xml(file_name, binary_data) diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index cc36484cd..c50b516f3 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -114,6 +114,7 @@ export interface ProcessFile { size: number; type: string; file_contents?: string; + file_contents_hash?: string; } export interface ProcessInstanceMetadata { diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index c82bc7a2f..6cd81fb7d 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -206,6 +206,9 @@ export default function ProcessModelEditDiagram() { httpMethod = 'POST'; } else { url += `/${fileNameWithExtension}`; + if (processModelFile && processModelFile.file_contents_hash) { + url += `?file_contents_hash=${processModelFile.file_contents_hash}`; + } } if (!fileNameWithExtension) { handleShowFileNameEditor();