supaapps-platform93-webhooks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .verifier import (
|
|
2
|
+
PLATFORM_EVENT_VERSIONS,
|
|
3
|
+
Platform93Event,
|
|
4
|
+
PermissionGrantLifecycleData,
|
|
5
|
+
assert_supported_platform_event,
|
|
6
|
+
dispatch_event,
|
|
7
|
+
is_custom_event,
|
|
8
|
+
is_platform_event,
|
|
9
|
+
verify_webhook,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PLATFORM_EVENT_VERSIONS",
|
|
14
|
+
"Platform93Event",
|
|
15
|
+
"PermissionGrantLifecycleData",
|
|
16
|
+
"assert_supported_platform_event",
|
|
17
|
+
"dispatch_event",
|
|
18
|
+
"is_custom_event",
|
|
19
|
+
"is_platform_event",
|
|
20
|
+
"verify_webhook",
|
|
21
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Platform93Event(TypedDict):
|
|
9
|
+
specversion: Literal["1.0"]
|
|
10
|
+
id: str
|
|
11
|
+
source: str
|
|
12
|
+
type: str
|
|
13
|
+
contract_source: Literal["platform93", "application"]
|
|
14
|
+
time: str
|
|
15
|
+
application_id: str
|
|
16
|
+
schema_version: str
|
|
17
|
+
subject: str | None
|
|
18
|
+
actor: dict[str, Any] | None
|
|
19
|
+
correlation_id: str | None
|
|
20
|
+
causation_id: str | None
|
|
21
|
+
data: dict[str, Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PermissionGrantLifecycleData(TypedDict):
|
|
25
|
+
grant_id: str
|
|
26
|
+
subject_type: Literal["user", "client"]
|
|
27
|
+
subject_id: str
|
|
28
|
+
workspace_id: str | None
|
|
29
|
+
permission: str
|
|
30
|
+
canonical_scope: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ControlUserIdentityData(TypedDict):
|
|
34
|
+
control_user_id: str
|
|
35
|
+
provider: Literal["google", "apple", "microsoft", "facebook", "linkedin"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ControlInvitationData(TypedDict, total=False):
|
|
39
|
+
invitation_id: str
|
|
40
|
+
organization_id: str | None
|
|
41
|
+
control_user_id: str
|
|
42
|
+
role: Literal["owner", "admin", "member", "auditor"]
|
|
43
|
+
onboarding_method: Literal["email", "google", "apple", "microsoft", "facebook", "linkedin"]
|
|
44
|
+
status: Literal["pending", "accepted", "revoked"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
PLATFORM_EVENT_VERSIONS = dict.fromkeys((
|
|
48
|
+
"organization.created", "organization.retired", "organization.restored",
|
|
49
|
+
"application.created", "application.retired", "application.restored",
|
|
50
|
+
"authorization.permission_grant.created", "authorization.permission_grant.revoked",
|
|
51
|
+
"control_user.identity_linked", "control_user.identity_unlinked",
|
|
52
|
+
"control_user.invitation_created", "control_user.invitation_resent",
|
|
53
|
+
"control_user.invitation_revoked", "control_user.invitation_accepted",
|
|
54
|
+
"control_auth.policy_updated", "control_auth.provider_login_enabled", "control_auth.provider_login_disabled",
|
|
55
|
+
"delegation.created", "delegation.exchanged", "delegation.revoked",
|
|
56
|
+
"entitlement.granted", "local_entitlement_request.created", "local_entitlement_request.approved",
|
|
57
|
+
"oauth.consent_revoked", "platform93.webhook.test", "user.created",
|
|
58
|
+
"storage.object.upload_requested", "storage.object.ready", "storage.object.deleted",
|
|
59
|
+
"storage.provider.verified", "storage.provider.disabled",
|
|
60
|
+
"user.email_verified", "user.email_unverified", "user.organization_verified",
|
|
61
|
+
"user.organization_unverified", "user.email_changed", "user.password_reset",
|
|
62
|
+
"user.pending_deletion", "user.anonymized", "user.deleted", "user.suspended", "user.restored",
|
|
63
|
+
"workspace.invitation_created", "workspace.invitation_accepted", "workspace.owner_transferred",
|
|
64
|
+
"application_invitation.created", "application_invitation.resent", "application_invitation.revoked",
|
|
65
|
+
"application_invitation.accepted", "application_invitation.expired", "user.updated",
|
|
66
|
+
"workspace.created", "workspace.updated", "workspace.archived", "workspace.member_added",
|
|
67
|
+
"workspace.member_updated", "workspace.member_removed", "entitlement.adjusted", "entitlement.revoked",
|
|
68
|
+
"entitlement.restored", "entitlement.expired", "entitlement.effective_changed",
|
|
69
|
+
"billing.subscription.updated", "billing.invoice.updated", "billing.payment.updated",
|
|
70
|
+
"billing.refund.updated", "billing.dispute.updated",
|
|
71
|
+
), "1.0")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def verify_webhook(
|
|
75
|
+
raw_body: bytes,
|
|
76
|
+
header: str,
|
|
77
|
+
secret: str,
|
|
78
|
+
tolerance_seconds: int = 300,
|
|
79
|
+
now: float | None = None,
|
|
80
|
+
) -> Platform93Event:
|
|
81
|
+
values: dict[str, list[str]] = {}
|
|
82
|
+
for part in header.split(","):
|
|
83
|
+
if "=" in part:
|
|
84
|
+
key, value = part.split("=", 1)
|
|
85
|
+
values.setdefault(key, []).append(value)
|
|
86
|
+
timestamp = int(values.get("t", ["0"])[0])
|
|
87
|
+
current = time.time() if now is None else now
|
|
88
|
+
if not timestamp or abs(current - timestamp) > tolerance_seconds:
|
|
89
|
+
raise ValueError("Platform93 webhook timestamp rejected")
|
|
90
|
+
expected = hmac.new(
|
|
91
|
+
secret.encode(), str(timestamp).encode() + b"." + raw_body, hashlib.sha256
|
|
92
|
+
).hexdigest()
|
|
93
|
+
if not any(
|
|
94
|
+
hmac.compare_digest(expected, signature) for signature in values.get("v1", [])
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("Platform93 webhook signature rejected")
|
|
97
|
+
event = json.loads(raw_body)
|
|
98
|
+
if (
|
|
99
|
+
event.get("specversion") != "1.0"
|
|
100
|
+
or not isinstance(event.get("type"), str)
|
|
101
|
+
or event.get("contract_source") not in ("platform93", "application")
|
|
102
|
+
or not isinstance(event.get("schema_version"), str)
|
|
103
|
+
or not isinstance(event.get("data"), dict)
|
|
104
|
+
):
|
|
105
|
+
raise ValueError("Platform93 webhook envelope rejected")
|
|
106
|
+
return event
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_platform_event(event: Platform93Event) -> bool:
|
|
110
|
+
return event["contract_source"] == "platform93" and event["type"] in PLATFORM_EVENT_VERSIONS
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_custom_event(event: Platform93Event) -> bool:
|
|
114
|
+
return event["contract_source"] == "application"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def assert_supported_platform_event(event: Platform93Event) -> None:
|
|
118
|
+
if not is_platform_event(event):
|
|
119
|
+
raise ValueError("Unknown Platform93 event contract")
|
|
120
|
+
supported = PLATFORM_EVENT_VERSIONS[event["type"]]
|
|
121
|
+
if event["schema_version"].split(".", 1)[0] != supported.split(".", 1)[0]:
|
|
122
|
+
raise ValueError("Unsupported Platform93 event schema version")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def dispatch_event(event: Platform93Event, handlers: dict[str, Any], custom: Any = None) -> bool:
|
|
126
|
+
if is_custom_event(event):
|
|
127
|
+
if custom is None:
|
|
128
|
+
return False
|
|
129
|
+
custom(event)
|
|
130
|
+
return True
|
|
131
|
+
assert_supported_platform_event(event)
|
|
132
|
+
handler = handlers.get(event["type"])
|
|
133
|
+
if handler is None:
|
|
134
|
+
return False
|
|
135
|
+
handler(event)
|
|
136
|
+
return True
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: supaapps-platform93-webhooks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Platform93 webhook signature verification
|
|
5
|
+
Project-URL: Homepage, https://github.com/supaapps/platform93
|
|
6
|
+
Project-URL: Repository, https://github.com/supaapps/platform93
|
|
7
|
+
Project-URL: Issues, https://github.com/supaapps/platform93/issues
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# supaapps-platform93-webhooks
|
|
13
|
+
|
|
14
|
+
Verification helpers for timestamped Platform93 webhook signatures over the exact raw
|
|
15
|
+
request body.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install supaapps-platform93-webhooks
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
See the [Platform93 repository](https://github.com/supaapps/platform93) for event
|
|
22
|
+
contracts and integration documentation.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
platform93_webhooks/__init__.py,sha256=qL96vd0Jadg371Y157hq0x31kaYEND1pjZucRtF0bt0,467
|
|
2
|
+
platform93_webhooks/verifier.py,sha256=5Rm3avNT5oR9EKSXDW6m6NM5DMn1FJGEF3icY8MjZ20,5501
|
|
3
|
+
supaapps_platform93_webhooks-0.1.0.dist-info/METADATA,sha256=g9dEZSM_J9TjDJFyxlupgR3mS-OUFBmFhXBe--ZoiEs,722
|
|
4
|
+
supaapps_platform93_webhooks-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
supaapps_platform93_webhooks-0.1.0.dist-info/RECORD,,
|