refactor webhook signature (#3889)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union
|
||||
|
||||
@@ -8,6 +10,20 @@ from jose import jwt
|
||||
from skyvern.config import settings
|
||||
|
||||
|
||||
def _normalize_numbers(x: Any) -> Any:
|
||||
if isinstance(x, float):
|
||||
return int(x) if x.is_integer() else x
|
||||
if isinstance(x, dict):
|
||||
return {k: _normalize_numbers(v) for k, v in x.items()}
|
||||
if isinstance(x, list):
|
||||
return [_normalize_numbers(v) for v in x]
|
||||
return x
|
||||
|
||||
|
||||
def _normalize_json_dumps(payload: dict) -> str:
|
||||
return json.dumps(_normalize_numbers(payload), separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any],
|
||||
expires_delta: timedelta | None = None,
|
||||
@@ -43,11 +59,25 @@ def generate_skyvern_signature(
|
||||
return hash_obj.hexdigest()
|
||||
|
||||
|
||||
def generate_skyvern_webhook_headers(payload: str, api_key: str) -> dict[str, str]:
|
||||
signature = generate_skyvern_signature(payload=payload, api_key=api_key)
|
||||
@dataclass
|
||||
class WebhookSignature:
|
||||
timestamp: str
|
||||
signature: str
|
||||
signed_payload: str
|
||||
headers: dict[str, str]
|
||||
|
||||
|
||||
def generate_skyvern_webhook_signature(payload: dict, api_key: str) -> WebhookSignature:
|
||||
payload_str = _normalize_json_dumps(payload)
|
||||
signature = generate_skyvern_signature(payload=payload_str, api_key=api_key)
|
||||
timestamp = str(int(datetime.utcnow().timestamp()))
|
||||
return {
|
||||
"x-skyvern-timestamp": timestamp,
|
||||
"x-skyvern-signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return WebhookSignature(
|
||||
timestamp=timestamp,
|
||||
signature=signature,
|
||||
signed_payload=payload_str,
|
||||
headers={
|
||||
"x-skyvern-timestamp": timestamp,
|
||||
"x-skyvern-signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ from skyvern.exceptions import (
|
||||
WorkflowRunNotFound,
|
||||
)
|
||||
from skyvern.forge import app
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_webhook_headers
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_webhook_signature
|
||||
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
|
||||
from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization
|
||||
@@ -135,7 +135,7 @@ async def test_webhook(
|
||||
)
|
||||
api_key = api_key_obj.token if api_key_obj else "test_api_key_placeholder"
|
||||
|
||||
headers = generate_skyvern_webhook_headers(payload=payload, api_key=api_key)
|
||||
signed_data = generate_skyvern_webhook_signature(payload=payload, api_key=api_key)
|
||||
|
||||
# Send the webhook request
|
||||
status_code = None
|
||||
@@ -146,8 +146,8 @@ async def test_webhook(
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
validated_url,
|
||||
content=payload,
|
||||
headers=headers,
|
||||
content=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
timeout=httpx.Timeout(10.0),
|
||||
)
|
||||
status_code = response.status_code
|
||||
@@ -190,7 +190,7 @@ async def test_webhook(
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
response_body=response_body,
|
||||
headers_sent=headers,
|
||||
headers_sent=signed_data.headers,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from skyvern.forge import app
|
||||
from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.artifact.models import ArtifactType
|
||||
from skyvern.forge.sdk.core import skyvern_context
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_webhook_headers
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_webhook_signature
|
||||
from skyvern.forge.sdk.core.skyvern_context import SkyvernContext
|
||||
from skyvern.forge.sdk.db.enums import TaskType
|
||||
from skyvern.forge.sdk.models import Step, StepStatus
|
||||
@@ -2076,12 +2076,11 @@ class WorkflowService:
|
||||
),
|
||||
errors=workflow_run_status_response.errors,
|
||||
)
|
||||
payload_dict = json.loads(workflow_run_status_response.model_dump_json())
|
||||
payload_dict: dict = json.loads(workflow_run_status_response.model_dump_json())
|
||||
workflow_run_response_dict = json.loads(workflow_run_response.model_dump_json())
|
||||
payload_dict.update(workflow_run_response_dict)
|
||||
payload = json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
headers = generate_skyvern_webhook_headers(
|
||||
payload=payload,
|
||||
signed_data = generate_skyvern_webhook_signature(
|
||||
payload=payload_dict,
|
||||
api_key=api_key,
|
||||
)
|
||||
LOG.info(
|
||||
@@ -2089,13 +2088,16 @@ class WorkflowService:
|
||||
workflow_id=workflow_id,
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
webhook_callback_url=workflow_run.webhook_callback_url,
|
||||
payload=payload,
|
||||
headers=headers,
|
||||
payload=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
url=workflow_run.webhook_callback_url, data=payload, headers=headers, timeout=httpx.Timeout(30.0)
|
||||
url=workflow_run.webhook_callback_url,
|
||||
data=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
timeout=httpx.Timeout(30.0),
|
||||
)
|
||||
if resp.status_code >= 200 and resp.status_code < 300:
|
||||
LOG.info(
|
||||
@@ -2114,7 +2116,7 @@ class WorkflowService:
|
||||
"Webhook failed",
|
||||
workflow_id=workflow_id,
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
webhook_data=payload,
|
||||
webhook_data=signed_data.signed_payload,
|
||||
resp=resp,
|
||||
resp_code=resp.status_code,
|
||||
resp_text=resp.text,
|
||||
|
||||
Reference in New Issue
Block a user