added secret verification to webhook endpoint w/ burnettk
This commit is contained in:
parent
d7221690f0
commit
6122fb0ae5
|
@ -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.
|
# 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.
|
# 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_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO")
|
||||||
GIT_CLONE_URL_FOR_PUBLISHING = environ.get("GIT_CLONE_URL", default=None)
|
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"
|
GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true"
|
||||||
|
|
||||||
# Datbase Configuration
|
# Datbase Configuration
|
||||||
|
|
|
@ -17,3 +17,5 @@ GIT_CLONE_URL_FOR_PUBLISHING = environ.get(
|
||||||
)
|
)
|
||||||
GIT_USERNAME = "sartography-automated-committer"
|
GIT_USERNAME = "sartography-automated-committer"
|
||||||
GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com"
|
GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com"
|
||||||
|
GIT_BRANCH_TO_PUBLISH_TO = "main"
|
||||||
|
GIT_BRANCH = "main"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""staging."""
|
||||||
|
GIT_BRANCH = "staging"
|
||||||
|
GIT_BRANCH_TO_PUBLISH_TO = "main"
|
||||||
|
GIT_COMMIT_ON_SAVE = False
|
|
@ -1826,8 +1826,12 @@ def get_spiff_task_from_process_instance(
|
||||||
# where 7000 is the port the app is running on locally
|
# where 7000 is the port the app is running on locally
|
||||||
def github_webhook_receive(body: Dict) -> Response:
|
def github_webhook_receive(body: Dict) -> Response:
|
||||||
"""Github_webhook_receive."""
|
"""Github_webhook_receive."""
|
||||||
GitService.handle_web_hook(body)
|
auth_header = request.headers.get("X-Hub-Signature-256")
|
||||||
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
AuthorizationService.verify_sha256_token(auth_header)
|
||||||
|
result = GitService.handle_web_hook(body)
|
||||||
|
return Response(
|
||||||
|
json.dumps({"git_pull": result}), status=200, mimetype="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""Authorization_service."""
|
"""Authorization_service."""
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
|
from hashlib import sha256
|
||||||
|
from hmac import compare_digest
|
||||||
|
from hmac import HMAC
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -45,6 +48,27 @@ class UserDoesNotHaveAccessToTaskError(Exception):
|
||||||
class AuthorizationService:
|
class AuthorizationService:
|
||||||
"""Determine whether a user has permission to perform their request."""
|
"""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
|
@classmethod
|
||||||
def has_permission(
|
def has_permission(
|
||||||
cls, principals: list[PrincipalModel], permission: str, target_uri: str
|
cls, principals: list[PrincipalModel], permission: str, target_uri: str
|
||||||
|
|
|
@ -140,7 +140,7 @@ class GitService:
|
||||||
|
|
||||||
# only supports github right now
|
# only supports github right now
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle_web_hook(cls, webhook: dict) -> None:
|
def handle_web_hook(cls, webhook: dict) -> bool:
|
||||||
"""Handle_web_hook."""
|
"""Handle_web_hook."""
|
||||||
cls.check_for_configs()
|
cls.check_for_configs()
|
||||||
|
|
||||||
|
@ -155,8 +155,25 @@ class GitService:
|
||||||
f"Configured clone url does not match clone url from webhook: {clone_url}"
|
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"]):
|
with FileSystemService.cd(current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]):
|
||||||
cls.run_shell_command(["git", "pull"])
|
cls.run_shell_command(["git", "pull"])
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def publish(cls, process_model_id: str, branch_to_update: str) -> str:
|
def publish(cls, process_model_id: str, branch_to_update: str) -> str:
|
||||||
|
|
|
@ -9,7 +9,6 @@ import pytest
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
from flask_bpmn.models.db import db
|
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.base_test import BaseTest
|
||||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
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.models.user import UserModel
|
||||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
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 (
|
from spiffworkflow_backend.services.process_instance_processor import (
|
||||||
ProcessInstanceProcessor,
|
ProcessInstanceProcessor,
|
||||||
)
|
)
|
||||||
|
@ -2573,7 +2573,8 @@ class TestProcessApi(BaseTest):
|
||||||
assert "On branch main" in output
|
assert "On branch main" in output
|
||||||
assert "No commits yet" in output
|
assert "No commits yet" in output
|
||||||
assert (
|
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"
|
process_group_id = "test_group"
|
||||||
|
@ -2593,7 +2594,9 @@ class TestProcessApi(BaseTest):
|
||||||
bpmn_file_name=bpmn_file_name,
|
bpmn_file_name=bpmn_file_name,
|
||||||
bpmn_file_location=bpmn_file_location,
|
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"])
|
output = GitService.run_shell_command_to_get_stdout(["git", "status"])
|
||||||
test_string = 'Untracked files:\n (use "git add <file>..." to include in what will be committed)\n\ttest_group'
|
test_string = 'Untracked files:\n (use "git add <file>..." to include in what will be committed)\n\ttest_group'
|
||||||
|
|
Loading…
Reference in New Issue