Verify GitHub webhook signature
This commit is contained in:
parent
b89a4a0fe8
commit
dff8bbed02
6 changed files with 44 additions and 2 deletions
|
|
@ -7,4 +7,5 @@ RUNBOAT_BUILD_ENV={}
|
||||||
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"}
|
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"}
|
||||||
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"}
|
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"}
|
||||||
RUNBOAT_GITHUB_TOKEN=
|
RUNBOAT_GITHUB_TOKEN=
|
||||||
|
RUNBOAT_GITHUB_WEBHOOK_SECRET=
|
||||||
RUNBOAT_LOG_CONFIG=log-config.yaml
|
RUNBOAT_LOG_CONFIG=log-config.yaml
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ dynamic = ["version", "description"]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"pytest-asyncio",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-dotenv",
|
"pytest-dotenv",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
|
|
@ -42,7 +43,7 @@ profile = 'black'
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
env_override_existing_values = 1
|
env_override_existing_values = 1
|
||||||
env_files = [".env.test"]
|
env_files = [".env.test"]
|
||||||
|
asyncio_mode = "strict"
|
||||||
# flake8 config is in .flake8
|
# flake8 config is in .flake8
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pluggy==1.0.0
|
||||||
py==1.11.0
|
py==1.11.0
|
||||||
pyparsing==3.0.7
|
pyparsing==3.0.7
|
||||||
pytest==6.2.5
|
pytest==6.2.5
|
||||||
|
pytest-asyncio==0.17.2
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-dotenv==0.5.2
|
pytest-dotenv==0.5.2
|
||||||
pytest-mock==3.6.1
|
pytest-mock==3.6.1
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ class Settings(BaseSettings):
|
||||||
# The token to use for the GitHub api calls (to query branches and pull requests,
|
# The token to use for the GitHub api calls (to query branches and pull requests,
|
||||||
# and report build statuses).
|
# and report build statuses).
|
||||||
github_token: str | None
|
github_token: str | None
|
||||||
|
# The secret used to verify GitHub webhook signatures
|
||||||
|
github_webhook_secret: bytes | None
|
||||||
# The file with the python logging configuration to use for the runboat controller.
|
# The file with the python logging configuration to use for the runboat controller.
|
||||||
log_config: str | None
|
log_config: str | None
|
||||||
# The base url where the runboat UI and API is exposed on internet.
|
# The base url where the runboat UI and API is exposed on internet.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Header, Request
|
from fastapi import APIRouter, BackgroundTasks, Header, Request
|
||||||
|
|
@ -11,13 +12,33 @@ _logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_github_signature(
|
||||||
|
x_hub_signature_256: str | None, secret: bytes | None, body: bytes
|
||||||
|
) -> bool:
|
||||||
|
if not secret:
|
||||||
|
return True
|
||||||
|
if not x_hub_signature_256:
|
||||||
|
_logger.warning("Got payload without X-Hub-Signature-256")
|
||||||
|
return False
|
||||||
|
signature = "sha256=" + hmac.new(secret, body, "sha256").hexdigest()
|
||||||
|
if not hmac.compare_digest(signature, x_hub_signature_256):
|
||||||
|
_logger.warning("Got payload with invalid X-Hub-Signature-256")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.post("/webhooks/github")
|
@router.post("/webhooks/github")
|
||||||
async def receive_payload(
|
async def receive_payload(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
request: Request,
|
request: Request,
|
||||||
x_github_event: str = Header(...),
|
x_github_event: str = Header(...),
|
||||||
|
x_hub_signature_256: str | None = Header(None),
|
||||||
) -> None:
|
) -> None:
|
||||||
# TODO check x-hub-signature
|
body = await request.body()
|
||||||
|
if not _verify_github_signature(
|
||||||
|
x_hub_signature_256, settings.github_webhook_secret, body
|
||||||
|
):
|
||||||
|
return
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
if x_github_event == "pull_request":
|
if x_github_event == "pull_request":
|
||||||
repo = payload["repository"]["full_name"]
|
repo = payload["repository"]["full_name"]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
|
||||||
from runboat.app import app
|
from runboat.app import app
|
||||||
from runboat.controller import controller
|
from runboat.controller import controller
|
||||||
from runboat.github import CommitInfo
|
from runboat.github import CommitInfo
|
||||||
|
from runboat.webhooks import _verify_github_signature
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
@ -137,3 +138,18 @@ def test_webhook_github_pr_close(mocker: MockerFixture) -> None:
|
||||||
repo="oca/mis-builder",
|
repo="oca/mis-builder",
|
||||||
pr=381,
|
pr=381,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_github_signature() -> None:
|
||||||
|
assert _verify_github_signature(None, None, b"body") # no secret configured, ok
|
||||||
|
assert not _verify_github_signature(
|
||||||
|
None, b"secret", b"body"
|
||||||
|
) # no X-Hub-Signature-256
|
||||||
|
assert not _verify_github_signature(
|
||||||
|
"sha256=invalid-sig", b"secret", b"body"
|
||||||
|
) # no X-Hub-Signature-256
|
||||||
|
assert _verify_github_signature(
|
||||||
|
"sha256=dc46983557fea127b43af721467eb9b3fde2338fe3e14f51952aa8478c13d355",
|
||||||
|
b"secret",
|
||||||
|
b"body",
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue