From 6122fb0ae5a663747744514e3a5feafd41921b2b Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 9 Dec 2022 16:51:00 -0500 Subject: [PATCH] added secret verification to webhook endpoint w/ burnettk --- .../spiffworkflow_backend/config/default.py | 5 ++-- .../config/development.py | 2 ++ .../spiffworkflow_backend/config/staging.py | 4 ++++ .../routes/process_api_blueprint.py | 8 +++++-- .../services/authorization_service.py | 24 +++++++++++++++++++ .../services/git_service.py | 19 ++++++++++++++- .../integration/test_process_api.py | 9 ++++--- 7 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/config/staging.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index a08ea5510..d0d6a4010 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -61,8 +61,9 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get( # When a user clicks on the `Publish` button, this is the default branch this server merges into. # I.e., dev server could have `staging` here. Staging server might have `production` here. -GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging") -GIT_CLONE_URL_FOR_PUBLISHING = environ.get("GIT_CLONE_URL", default=None) +GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO") +GIT_BRANCH = environ.get("GIT_BRANCH") +GIT_CLONE_URL_FOR_PUBLISHING = environ.get("GIT_CLONE_URL") GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true" # Datbase Configuration diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py index 39e10cb58..15cbead83 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/development.py @@ -17,3 +17,5 @@ GIT_CLONE_URL_FOR_PUBLISHING = environ.get( ) GIT_USERNAME = "sartography-automated-committer" GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com" +GIT_BRANCH_TO_PUBLISH_TO = "main" +GIT_BRANCH = "main" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/staging.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/staging.py new file mode 100644 index 000000000..bd834cb7f --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/staging.py @@ -0,0 +1,4 @@ +"""staging.""" +GIT_BRANCH = "staging" +GIT_BRANCH_TO_PUBLISH_TO = "main" +GIT_COMMIT_ON_SAVE = False 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 37dceb842..3e7eed530 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -1826,8 +1826,12 @@ def get_spiff_task_from_process_instance( # where 7000 is the port the app is running on locally def github_webhook_receive(body: Dict) -> Response: """Github_webhook_receive.""" - GitService.handle_web_hook(body) - return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + auth_header = request.headers.get("X-Hub-Signature-256") + AuthorizationService.verify_sha256_token(auth_header) + result = GitService.handle_web_hook(body) + return Response( + json.dumps({"git_pull": result}), status=200, mimetype="application/json" + ) # diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 511d138eb..9456f8f14 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -1,6 +1,9 @@ """Authorization_service.""" import inspect import re +from hashlib import sha256 +from hmac import compare_digest +from hmac import HMAC from typing import Optional from typing import Union @@ -45,6 +48,27 @@ class UserDoesNotHaveAccessToTaskError(Exception): class AuthorizationService: """Determine whether a user has permission to perform their request.""" + # https://stackoverflow.com/a/71320673/6090676 + @classmethod + def verify_sha256_token(cls, auth_header: Optional[str]) -> None: + """Verify_sha256_token.""" + if auth_header is None: + raise ApiError( + error_code="unauthorized", + message="", + status_code=403, + ) + + received_sign = auth_header.split("sha256=")[-1].strip() + secret = current_app.config["GITHUB_WEBHOOK_SECRET"].encode() + expected_sign = HMAC(key=secret, msg=request.data, digestmod=sha256).hexdigest() + if not compare_digest(received_sign, expected_sign): + raise ApiError( + error_code="unauthorized", + message="", + status_code=403, + ) + @classmethod def has_permission( cls, principals: list[PrincipalModel], permission: str, target_uri: str diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py index ac9fe490d..582cf064b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/git_service.py @@ -140,7 +140,7 @@ class GitService: # only supports github right now @classmethod - def handle_web_hook(cls, webhook: dict) -> None: + def handle_web_hook(cls, webhook: dict) -> bool: """Handle_web_hook.""" cls.check_for_configs() @@ -155,8 +155,25 @@ class GitService: f"Configured clone url does not match clone url from webhook: {clone_url}" ) + if "ref" not in webhook: + raise InvalidGitWebhookBodyError( + f"Could not find the 'ref' arg in the webhook boy: {webhook}" + ) + + if current_app.config["GIT_BRANCH"] is None: + raise MissingGitConfigsError( + "Missing config for GIT_BRANCH. " + "This is required for updating the repository as a result of the webhook" + ) + + ref = webhook["ref"] + git_branch = current_app.config["GIT_BRANCH"] + if ref != f"refs/heads/{git_branch}": + return False + with FileSystemService.cd(current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]): cls.run_shell_command(["git", "pull"]) + return True @classmethod def publish(cls, process_model_id: str, branch_to_update: str) -> str: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index 313f91d63..3f171f9fe 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -9,7 +9,6 @@ import pytest from flask.app import Flask from flask.testing import FlaskClient from flask_bpmn.models.db import db -from spiffworkflow_backend.services.git_service import GitService from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec @@ -33,6 +32,7 @@ from spiffworkflow_backend.models.spec_reference import SpecReferenceCache from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -2573,7 +2573,8 @@ class TestProcessApi(BaseTest): assert "On branch main" in output assert "No commits yet" in output assert ( - 'nothing to commit (create/copy files and use "git add" to track)' in output + 'nothing to commit (create/copy files and use "git add" to track)' + in output ) process_group_id = "test_group" @@ -2593,7 +2594,9 @@ class TestProcessApi(BaseTest): bpmn_file_name=bpmn_file_name, bpmn_file_location=bpmn_file_location, ) - process_model_absolute_dir = os.path.join(bpmn_root, process_model_identifier) + process_model_absolute_dir = os.path.join( + bpmn_root, process_model_identifier + ) output = GitService.run_shell_command_to_get_stdout(["git", "status"]) test_string = 'Untracked files:\n (use "git add ..." to include in what will be committed)\n\ttest_group'