Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Add EventGrid compatible webhook format #1640

Merged
merged 5 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/webhook_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ Each event will be submitted via HTTP POST to the user provided URL.
}
```

## Event Grid Payload format

If webhook is set to have Event Grid message format then the payload will look as follows:

### Example

```json
[
{
"data": {
"ping_id": "00000000-0000-0000-0000-000000000000"
},
"dataVersion": "1.0.0",
"eventTime": "0001-01-01T00:00:00",
"eventType": "ping",
"id": "00000000-0000-0000-0000-000000000000",
"subject": "example"
}
]
```

## Event Types (EventType)

* [crash_reported](#crash_reported)
Expand Down
15 changes: 15 additions & 0 deletions docs/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ $ onefuzz webhooks create MYWEBHOOK https://contoso.com/my-custom-webhook task_c
$
```

Example creating a webhook subscription only the `task_created` events that produces webhook data in [Azure Event Grid](https://docs.microsoft.com/en-us/azure/event-grid/event-schema) compatible format:

```
$ onefuzz webhooks create MYWEBHOOK https://contoso.com/my-custom-webhook task_created --message_format event_grid
{
"webhook_id": "cc6926de-7c6f-487e-96ec-7b632d3ed52b",
"name": "MYWEBHOOK",
"event_types": [
"task_created"
]
}
$
```


### Listing existing webhooks

```
Expand Down
47 changes: 35 additions & 12 deletions src/api-service/__app__/onefuzzlib/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import datetime
import hmac
import json
import logging
from hashlib import sha512
from typing import List, Optional, Tuple
Expand All @@ -16,7 +17,11 @@
from onefuzztypes.events import Event, EventMessage, EventPing, EventType
from onefuzztypes.models import Error, Result
from onefuzztypes.webhooks import Webhook as BASE_WEBHOOK
from onefuzztypes.webhooks import WebhookMessage
from onefuzztypes.webhooks import (
WebhookMessage,
WebhookMessageEventGrid,
WebhookMessageFormat,
)
from onefuzztypes.webhooks import WebhookMessageLog as BASE_WEBHOOK_MESSAGE_LOG
from pydantic import BaseModel, Field

Expand Down Expand Up @@ -203,6 +208,7 @@ def send(self, message_log: WebhookMessageLog) -> bool:
event_type=message_log.event_type,
event=message_log.event,
secret_token=self.secret_token,
message_format=self.message_format,
)

headers = {"Content-type": "application/json", "User-Agent": USER_AGENT}
Expand All @@ -225,19 +231,36 @@ def build_message(
event_type: EventType,
event: Event,
secret_token: Optional[str] = None,
message_format: Optional[WebhookMessageFormat] = None,
) -> Tuple[bytes, Optional[str]]:
data = (
WebhookMessage(
webhook_id=webhook_id,
event_id=event_id,
event_type=event_type,
event=event,
instance_id=get_instance_id(),
instance_name=get_instance_name(),

if message_format and message_format == WebhookMessageFormat.event_grid:
decoded = [
json.loads(
WebhookMessageEventGrid(
id=event_id,
data=event,
dataVersion="1.0.0",
subject=get_instance_name(),
eventType=event_type,
eventTime=datetime.datetime.now(datetime.timezone.utc),
).json(sort_keys=True, exclude_none=True)
)
]
data = json.dumps(decoded).encode()
else:
data = (
WebhookMessage(
webhook_id=webhook_id,
event_id=event_id,
event_type=event_type,
event=event,
instance_id=get_instance_id(),
instance_name=get_instance_name(),
)
.json(sort_keys=True, exclude_none=True)
.encode()
)
.json(sort_keys=True, exclude_none=True)
.encode()
)
digest = None
if secret_token:
digest = hmac.new(secret_token.encode(), msg=data, digestmod=sha512).hexdigest()
Expand Down
6 changes: 6 additions & 0 deletions src/api-service/__app__/webhooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def post(req: func.HttpRequest) -> func.HttpResponse:
event_types=request.event_types,
secret_token=request.secret_token,
)
if request.message_format is not None:
webhook.message_format = request.message_format

webhook.save()

webhook.url = None
Expand Down Expand Up @@ -83,6 +86,9 @@ def patch(req: func.HttpRequest) -> func.HttpResponse:
if request.secret_token is not None:
webhook.secret_token = request.secret_token

if request.message_format is not None:
webhook.message_format = request.message_format

webhook.save()
webhook.url = None
webhook.secret_token = None
Expand Down
9 changes: 8 additions & 1 deletion src/cli/onefuzz/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,19 @@ def create(
event_types: List[events.EventType],
*,
secret_token: Optional[str] = None,
message_format: Optional[webhooks.WebhookMessageFormat] = None,
) -> webhooks.Webhook:
"""Create a webhook"""
self.logger.debug("creating webhook. name: %s", name)
return self._req_model(
"POST",
webhooks.Webhook,
data=requests.WebhookCreate(
name=name, url=url, event_types=event_types, secret_token=secret_token
name=name,
url=url,
event_types=event_types,
secret_token=secret_token,
message_format=message_format,
),
)

Expand All @@ -345,6 +350,7 @@ def update(
url: Optional[str] = None,
event_types: Optional[List[events.EventType]] = None,
secret_token: Optional[str] = None,
message_format: Optional[webhooks.WebhookMessageFormat] = None,
) -> webhooks.Webhook:
"""Update a webhook"""

Expand All @@ -362,6 +368,7 @@ def update(
url=url,
event_types=event_types,
secret_token=secret_token,
message_format=message_format,
),
)

Expand Down
38 changes: 37 additions & 1 deletion src/pytypes/extra/generate-docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import datetime
import json
import sys
from typing import List, Optional
from uuid import UUID
Expand Down Expand Up @@ -65,7 +67,7 @@
UserInfo,
)
from onefuzztypes.primitives import Container, PoolName, Region
from onefuzztypes.webhooks import WebhookMessage
from onefuzztypes.webhooks import WebhookMessage, WebhookMessageEventGrid

EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
ZERO_SHA256 = "0" * len(EMPTY_SHA256)
Expand Down Expand Up @@ -290,6 +292,25 @@ def main() -> None:
instance_name="example",
)

message_event_grid = WebhookMessageEventGrid(
dataVersion="1.0.0",
subject="example",
eventType=EventType.ping,
eventTime=datetime.datetime.min,
id=UUID(int=0),
data=EventPing(ping_id=UUID(int=0)),
)

message_event_grid_json = json.dumps(
[
json.loads(
message_event_grid.json(indent=4, exclude_none=True, sort_keys=True)
)
],
indent=4,
sort_keys=True,
)

result = ""
result += layer(
1,
Expand All @@ -309,6 +330,21 @@ def main() -> None:
message.json(indent=4, exclude_none=True, sort_keys=True),
"json",
)

result += layer(
2,
"Event Grid Payload format",
"If webhook is set to have Event Grid message format then "
"the payload will look as follows:",
)

result += typed(
3,
"Example",
message_event_grid_json,
"json",
)

result += layer(2, "Event Types (EventType)")

event_map = {get_event_type(x).name: x for x in examples}
Expand Down
3 changes: 3 additions & 0 deletions src/pytypes/onefuzztypes/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .events import EventType
from .models import AutoScaleConfig, InstanceConfig, NotificationConfig
from .primitives import Container, PoolName, Region
from .webhooks import WebhookMessageFormat


class BaseRequest(BaseModel):
Expand Down Expand Up @@ -211,6 +212,7 @@ class WebhookCreate(BaseRequest):
url: AnyHttpUrl
event_types: List[EventType]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]


class WebhookSearch(BaseModel):
Expand All @@ -227,6 +229,7 @@ class WebhookUpdate(BaseModel):
event_types: Optional[List[EventType]]
url: Optional[AnyHttpUrl]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]


class NodeAddSshKey(BaseModel):
Expand Down
19 changes: 18 additions & 1 deletion src/pytypes/onefuzztypes/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID, uuid4

from pydantic import AnyHttpUrl, BaseModel, Field

from .enums import WebhookMessageState
from .events import EventMessage, EventType
from .events import Event, EventMessage, EventType


class WebhookMessageFormat(Enum):
onefuzz = "onefuzz"
event_grid = "event_grid"


class WebhookMessage(EventMessage):
webhook_id: UUID


class WebhookMessageEventGrid(BaseModel):
dataVersion: str
subject: str
eventType: EventType
eventTime: datetime
id: UUID
data: Event


class WebhookMessageLog(WebhookMessage):
state: WebhookMessageState = Field(default=WebhookMessageState.queued)
try_count: int = Field(default=0)
Expand All @@ -27,3 +43,4 @@ class Webhook(BaseModel):
url: Optional[AnyHttpUrl]
event_types: List[EventType]
secret_token: Optional[str]
message_format: Optional[WebhookMessageFormat]