refactor webhook signature (#3889)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import structlog
|
||||
@@ -10,7 +9,7 @@ from skyvern.exceptions import FailedToGetTOTPVerificationCode, NoTOTPVerificati
|
||||
from skyvern.forge import app
|
||||
from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.core.aiohttp_helper import aiohttp_post
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_signature
|
||||
from skyvern.forge.sdk.core.security import generate_skyvern_webhook_signature
|
||||
from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType
|
||||
from skyvern.forge.sdk.schemas.totp_codes import OTPType
|
||||
|
||||
@@ -122,19 +121,12 @@ async def _get_otp_value_from_url(
|
||||
request_data["workflow_run_id"] = workflow_run_id
|
||||
if workflow_permanent_id:
|
||||
request_data["workflow_permanent_id"] = workflow_permanent_id
|
||||
payload = json.dumps(request_data)
|
||||
signature = generate_skyvern_signature(
|
||||
payload=payload,
|
||||
signed_data = generate_skyvern_webhook_signature(
|
||||
payload=request_data,
|
||||
api_key=api_key,
|
||||
)
|
||||
timestamp = str(int(datetime.utcnow().timestamp()))
|
||||
headers = {
|
||||
"x-skyvern-timestamp": timestamp,
|
||||
"x-skyvern-signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
json_resp = await aiohttp_post(url=url, data=request_data, headers=headers, raise_exception=False)
|
||||
json_resp = await aiohttp_post(url=url, data=request_data, headers=signed_data.headers, raise_exception=False)
|
||||
except Exception as e:
|
||||
LOG.error("Failed to get otp value from url", exc_info=True)
|
||||
raise FailedToGetTOTPVerificationCode(
|
||||
|
||||
@@ -20,7 +20,7 @@ from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory
|
||||
from skyvern.forge.sdk.artifact.models import ArtifactType
|
||||
from skyvern.forge.sdk.core import skyvern_context
|
||||
from skyvern.forge.sdk.core.hashing import generate_url_hash
|
||||
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 OrganizationAuthTokenType
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization
|
||||
@@ -1817,17 +1817,19 @@ async def send_task_v2_webhook(task_v2: TaskV2) -> None:
|
||||
payload_json = task_v2.model_dump_json(by_alias=True)
|
||||
payload_dict = json.loads(payload_json)
|
||||
payload_dict.update(json.loads(task_run_response_json))
|
||||
payload = json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
headers = generate_skyvern_webhook_headers(payload=payload, api_key=api_key.token)
|
||||
signed_data = generate_skyvern_webhook_signature(payload=payload_dict, api_key=api_key.token)
|
||||
LOG.info(
|
||||
"Sending task v2 response to webhook callback url",
|
||||
task_v2_id=task_v2.observer_cruise_id,
|
||||
webhook_callback_url=task_v2.webhook_callback_url,
|
||||
payload=payload,
|
||||
headers=headers,
|
||||
payload=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
)
|
||||
resp = await httpx.AsyncClient().post(
|
||||
task_v2.webhook_callback_url, data=payload, headers=headers, timeout=httpx.Timeout(30.0)
|
||||
task_v2.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(
|
||||
|
||||
@@ -20,7 +20,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.schemas.task_v2 import TaskV2
|
||||
from skyvern.forge.sdk.schemas.tasks import Task, TaskRequest, TaskResponse, TaskStatus
|
||||
@@ -51,7 +51,7 @@ def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def build_sample_task_payload(run_id: str | None = None) -> str:
|
||||
def build_sample_task_payload(run_id: str | None = None) -> dict:
|
||||
"""
|
||||
Build a sample task webhook payload using the real TaskResponse + TaskRunResponse models
|
||||
so schema changes are reflected automatically.
|
||||
@@ -128,10 +128,10 @@ def build_sample_task_payload(run_id: str | None = None) -> str:
|
||||
)
|
||||
|
||||
payload_dict.update(json.loads(task_run_response.model_dump_json(exclude_unset=True)))
|
||||
return json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
return payload_dict
|
||||
|
||||
|
||||
def build_sample_workflow_run_payload(run_id: str | None = None) -> str:
|
||||
def build_sample_workflow_run_payload(run_id: str | None = None) -> dict:
|
||||
"""
|
||||
Build a sample workflow webhook payload using the real WorkflowRunResponseBase + WorkflowRunResponse models
|
||||
so schema changes are reflected automatically.
|
||||
@@ -195,14 +195,14 @@ def build_sample_workflow_run_payload(run_id: str | None = None) -> str:
|
||||
)
|
||||
|
||||
payload_dict.update(json.loads(workflow_run_response.model_dump_json(exclude_unset=True)))
|
||||
return json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
return payload_dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class _WebhookPayload:
|
||||
run_id: str
|
||||
run_type: str
|
||||
payload: str
|
||||
payload: dict
|
||||
default_webhook_url: str | None
|
||||
|
||||
|
||||
@@ -210,13 +210,13 @@ async def build_run_preview(organization_id: str, run_id: str) -> RunWebhookPrev
|
||||
"""Return the payload and headers that would be used for a replay."""
|
||||
payload = await _build_webhook_payload(organization_id=organization_id, run_id=run_id)
|
||||
api_key = await _get_api_key(organization_id=organization_id)
|
||||
headers = generate_skyvern_webhook_headers(payload=payload.payload, api_key=api_key)
|
||||
signed_data = generate_skyvern_webhook_signature(payload=payload.payload, api_key=api_key)
|
||||
return RunWebhookPreviewResponse(
|
||||
run_id=payload.run_id,
|
||||
run_type=payload.run_type,
|
||||
default_webhook_url=payload.default_webhook_url,
|
||||
payload=payload.payload,
|
||||
headers=headers,
|
||||
payload=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
)
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ async def replay_run_webhook(organization_id: str, run_id: str, target_url: str
|
||||
"""
|
||||
payload = await _build_webhook_payload(organization_id=organization_id, run_id=run_id)
|
||||
api_key = await _get_api_key(organization_id=organization_id)
|
||||
headers = generate_skyvern_webhook_headers(payload=payload.payload, api_key=api_key)
|
||||
signed_data = generate_skyvern_webhook_signature(payload=payload.payload, api_key=api_key)
|
||||
|
||||
url_to_use: str | None = target_url if target_url else payload.default_webhook_url
|
||||
|
||||
@@ -237,8 +237,8 @@ async def replay_run_webhook(organization_id: str, run_id: str, target_url: str
|
||||
|
||||
status_code, latency_ms, response_body, error = await _deliver_webhook(
|
||||
url=validated_url,
|
||||
payload=payload.payload,
|
||||
headers=headers,
|
||||
payload=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
)
|
||||
|
||||
return RunWebhookReplayResponse(
|
||||
@@ -246,8 +246,8 @@ async def replay_run_webhook(organization_id: str, run_id: str, target_url: str
|
||||
run_type=payload.run_type,
|
||||
default_webhook_url=payload.default_webhook_url,
|
||||
target_webhook_url=validated_url,
|
||||
payload=payload.payload,
|
||||
headers=headers,
|
||||
payload=signed_data.signed_payload,
|
||||
headers=signed_data.headers,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
response_body=response_body,
|
||||
@@ -330,11 +330,10 @@ async def _build_task_payload(organization_id: str, run_id: str, run_type_str: s
|
||||
run_response_json = run_response.model_dump_json(exclude={"run_request"})
|
||||
payload_dict.update(json.loads(run_response_json))
|
||||
|
||||
payload = json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
return _WebhookPayload(
|
||||
run_id=run_id,
|
||||
run_type=run_type_str,
|
||||
payload=payload,
|
||||
payload=payload_dict,
|
||||
default_webhook_url=task.webhook_callback_url,
|
||||
)
|
||||
|
||||
@@ -360,12 +359,10 @@ async def _build_task_v2_payload(task_v2: TaskV2) -> _WebhookPayload:
|
||||
f"Run {task_v2.observer_cruise_id} has not reached a terminal state (status={task_run_response.status})."
|
||||
)
|
||||
task_run_response_json = task_run_response.model_dump_json(exclude={"run_request"})
|
||||
|
||||
payload = json.dumps(json.loads(task_run_response_json), separators=(",", ":"), ensure_ascii=False)
|
||||
return _WebhookPayload(
|
||||
run_id=task_v2.observer_cruise_id,
|
||||
run_type=RunType.task_v2.value,
|
||||
payload=payload,
|
||||
payload=json.loads(task_run_response_json),
|
||||
default_webhook_url=task_v2.webhook_callback_url,
|
||||
)
|
||||
|
||||
@@ -437,12 +434,11 @@ async def _build_workflow_payload(
|
||||
)
|
||||
)
|
||||
payload_dict.update(json.loads(run_response.model_dump_json(exclude={"run_request"})))
|
||||
payload = json.dumps(payload_dict, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
return _WebhookPayload(
|
||||
run_id=workflow_run.workflow_run_id,
|
||||
run_type=RunType.workflow_run.value,
|
||||
payload=payload,
|
||||
payload=payload_dict,
|
||||
default_webhook_url=workflow_run.webhook_callback_url,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user