Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type webhook methods #307

Merged
merged 6 commits into from
Jul 31, 2024
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
28 changes: 5 additions & 23 deletions tests/test_webhooks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import json
from os import error
from workos.webhooks import Webhooks
Expand All @@ -16,47 +17,28 @@ def setup(self, set_api_key):

@pytest.fixture
def mock_event_body(self):
return '{"id":"wh_01FG9JXJ9C9S052FX59JVG4EG1","data":{"id":"conn_01EHWNC0FCBHZ3BJ7EGKYXK0E6","name":"Foo Corp\'s Connection","state":"active","object":"connection","domains":[{"id":"conn_domain_01EHWNFTAFCF3CQAE5A9Q0P1YB","domain":"foo-corp.com","object":"connection_domain"}],"connection_type":"OktaSAML","organization_id":"org_01EHWNCE74X7JSDV0X3SZ3KJNY"},"event":"connection.activated"}'
return '{"id":"event_01J44T8116Q5M0RYCFA6KWNXN9","data":{"id":"conn_01EHWNC0FCBHZ3BJ7EGKYXK0E6","name":"Foo Corp\'s Connection","state":"active","object":"connection","status":"linked","domains":[{"id":"conn_domain_01EHWNFTAFCF3CQAE5A9Q0P1YB","domain":"foo-corp.com","object":"connection_domain"}],"created_at":"2021-06-25T19:07:33.155Z","updated_at":"2021-06-25T19:07:33.155Z","external_key":"3QMR4u0Tok6SgwY2AWG6u6mkQ","connection_type":"OktaSAML","organization_id":"org_01EHWNCE74X7JSDV0X3SZ3KJNY"},"event":"connection.activated","created_at":"2021-06-25T19:07:33.155Z"}'

@pytest.fixture
def mock_header(self):
return "t=1632409405772, v1=67612f0e74f008b436a13b00266f90ef5c13f9cbcf6262206f5f4a539ff61702"
return "t=1722443701539, v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5"

@pytest.fixture
def mock_secret(self):
return "1lyKDzhJjuCkIscIWqkSe4YsQ"
return "2sAZJlbjP8Ce3rwkKEv2GfKef"

@pytest.fixture
def mock_bad_secret(self):
return "this_is_not_it_123"

@pytest.fixture
def mock_header_no_timestamp(self):
return "v1=67612f0e74f008b436a13b00266f90ef5c13f9cbcf6262206f5f4a539ff61702"
return "v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5"

@pytest.fixture
def mock_sig_hash(self):
return "df25b6efdd39d82e7b30e75ea19655b306860ad5cde3eeaeb6f1dfea029ea259"

def test_missing_body(self, mock_header, mock_secret):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(None, mock_header, mock_secret)
assert "Payload body is missing and is a required parameter" in str(err.value)

def test_missing_header(self, mock_event_body, mock_secret):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(
mock_event_body.encode("utf-8"), None, mock_secret
)
assert "Payload signature missing and is a required parameter" in str(err.value)

def test_missing_secret(self, mock_event_body, mock_header):
with pytest.raises(ValueError) as err:
self.webhooks.verify_event(
mock_event_body.encode("utf-8"), mock_header, None
)
assert "Secret is missing and is a required parameter" in str(err.value)

def test_unable_to_extract_timestamp(
self, mock_event_body, mock_header_no_timestamp, mock_secret
):
Expand Down
283 changes: 283 additions & 0 deletions workos/resources/webhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
from typing import Generic, Literal, Union
from pydantic import Field
from typing_extensions import Annotated
from workos.resources.directory_sync import DirectoryGroup
from workos.resources.events import EventPayload
from workos.resources.user_management import OrganizationMembership, User
from workos.resources.workos_model import WorkOSModel
from workos.types.directory_sync.directory_user import DirectoryUser
from workos.types.events.authentication_payload import (
AuthenticationEmailVerificationSucceededPayload,
AuthenticationMagicAuthFailedPayload,
AuthenticationMagicAuthSucceededPayload,
AuthenticationMfaSucceededPayload,
AuthenticationOauthSucceededPayload,
AuthenticationPasswordFailedPayload,
AuthenticationPasswordSucceededPayload,
AuthenticationSsoSucceededPayload,
)
from workos.types.events.connection_payload_with_legacy_fields import (
ConnectionPayloadWithLegacyFields,
)
from workos.types.events.directory_group_membership_payload import (
DirectoryGroupMembershipPayload,
)
from workos.types.events.directory_group_with_previous_attributes import (
DirectoryGroupWithPreviousAttributes,
)
from workos.types.events.directory_payload import DirectoryPayload
from workos.types.events.directory_payload_with_legacy_fields import (
DirectoryPayloadWithLegacyFields,
)
from workos.types.events.directory_user_with_previous_attributes import (
DirectoryUserWithPreviousAttributes,
)
from workos.types.events.organization_domain_verification_failed_payload import (
OrganizationDomainVerificationFailedPayload,
)
from workos.types.events.session_created_payload import SessionCreatedPayload
from workos.types.organizations.organization_common import OrganizationCommon
from workos.types.organizations.organization_domain import OrganizationDomain
from workos.types.roles.role import Role
from workos.types.sso.connection import Connection
from workos.types.user_management.email_verification_common import (
EmailVerificationCommon,
)
from workos.types.user_management.invitation_common import InvitationCommon
from workos.types.user_management.magic_auth_common import MagicAuthCommon
from workos.types.user_management.password_reset_common import PasswordResetCommon


class WebhookModel(WorkOSModel, Generic[EventPayload]):
"""Representation of an Webhook delivered via Webhook.
Attributes:
OBJECT_FIELDS (list): List of fields an Webhook is comprised of.
Comment on lines +53 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can go since OBJECT_FIELDS doesn't exist anymore.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I plan to do a comment/docstring sweep across all the resources as a follow-up. Though, I haven't seen any place where these docstrings are surfaced to the developer. Do you know how they're usually used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see them in my editor, albeit poorly formatted:
image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. It seems like they're more useful the the actual SDK methods. Not to much for the resources/models. I have to dig into the packages to actually see the docstrings for our resources.
CleanShot 2024-07-31 at 10 52 18

"""

id: str
data: EventPayload
created_at: str
Comment on lines +51 to +59
Copy link
Author

@tribble tribble Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super-sad panda 😿

There is a difference between our webhooks and events. Each event contains object: 'event'. Our webhooks do not.

A mini future us project should be to add this object attribute to webhook bodies so that they're identical and we can remove this sort of duplication.



class AuthenticationEmailVerificationSucceededWebhook(
WebhookModel[AuthenticationEmailVerificationSucceededPayload,]
):
event: Literal["authentication.email_verification_succeeded"]


class AuthenticationMagicAuthFailedWebhook(
WebhookModel[AuthenticationMagicAuthFailedPayload,]
):
event: Literal["authentication.magic_auth_failed"]


class AuthenticationMagicAuthSucceededWebhook(
WebhookModel[AuthenticationMagicAuthSucceededPayload,]
):
event: Literal["authentication.magic_auth_succeeded"]


class AuthenticationMfaSucceededWebhook(
WebhookModel[AuthenticationMfaSucceededPayload]
):
event: Literal["authentication.mfa_succeeded"]


class AuthenticationOauthSucceededWebhook(
WebhookModel[AuthenticationOauthSucceededPayload]
):
event: Literal["authentication.oauth_succeeded"]


class AuthenticationPasswordFailedWebhook(
WebhookModel[AuthenticationPasswordFailedPayload]
):
event: Literal["authentication.password_failed"]


class AuthenticationPasswordSucceededWebhook(
WebhookModel[AuthenticationPasswordSucceededPayload,]
):
event: Literal["authentication.password_succeeded"]


class AuthenticationSsoSucceededWebhook(
WebhookModel[AuthenticationSsoSucceededPayload]
):
event: Literal["authentication.sso_succeeded"]


class ConnectionActivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]):
event: Literal["connection.activated"]


class ConnectionDeactivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]):
event: Literal["connection.deactivated"]


class ConnectionDeletedWebhook(WebhookModel[Connection]):
event: Literal["connection.deleted"]


class DirectoryActivatedWebhook(WebhookModel[DirectoryPayloadWithLegacyFields]):
event: Literal["dsync.activated"]


class DirectoryDeletedWebhook(WebhookModel[DirectoryPayload]):
event: Literal["dsync.deleted"]


class DirectoryGroupCreatedWebhook(WebhookModel[DirectoryGroup]):
event: Literal["dsync.group.created"]


class DirectoryGroupDeletedWebhook(WebhookModel[DirectoryGroup]):
event: Literal["dsync.group.deleted"]


class DirectoryGroupUpdatedWebhook(WebhookModel[DirectoryGroupWithPreviousAttributes]):
event: Literal["dsync.group.updated"]


class DirectoryUserCreatedWebhook(WebhookModel[DirectoryUser]):
event: Literal["dsync.user.created"]


class DirectoryUserDeletedWebhook(WebhookModel[DirectoryUser]):
event: Literal["dsync.user.deleted"]


class DirectoryUserUpdatedWebhook(WebhookModel[DirectoryUserWithPreviousAttributes]):
event: Literal["dsync.user.updated"]


class DirectoryUserAddedToGroupWebhook(WebhookModel[DirectoryGroupMembershipPayload]):
event: Literal["dsync.group.user_added"]


class DirectoryUserRemovedFromGroupWebhook(
WebhookModel[DirectoryGroupMembershipPayload]
):
event: Literal["dsync.group.user_removed"]


class EmailVerificationCreatedWebhook(WebhookModel[EmailVerificationCommon]):
event: Literal["email_verification.created"]


class InvitationCreatedWebhook(WebhookModel[InvitationCommon]):
event: Literal["invitation.created"]


class MagicAuthCreatedWebhook(WebhookModel[MagicAuthCommon]):
event: Literal["magic_auth.created"]


class OrganizationCreatedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.created"]


class OrganizationDeletedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.deleted"]


class OrganizationUpdatedWebhook(WebhookModel[OrganizationCommon]):
event: Literal["organization.updated"]


class OrganizationDomainVerificationFailedWebhook(
WebhookModel[OrganizationDomainVerificationFailedPayload,]
):
event: Literal["organization_domain.verification_failed"]


class OrganizationDomainVerifiedWebhook(WebhookModel[OrganizationDomain]):
event: Literal["organization_domain.verified"]


class OrganizationMembershipCreatedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.created"]


class OrganizationMembershipDeletedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.deleted"]


class OrganizationMembershipUpdatedWebhook(WebhookModel[OrganizationMembership]):
event: Literal["organization_membership.updated"]


class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]):
event: Literal["password_reset.created"]


class RoleCreatedWebhook(WebhookModel[Role]):
event: Literal["role.created"]


class RoleDeletedWebhook(WebhookModel[Role]):
event: Literal["role.deleted"]


class RoleUpdatedWebhook(WebhookModel[Role]):
event: Literal["role.updated"]


class SessionCreatedWebhook(WebhookModel[SessionCreatedPayload]):
event: Literal["session.created"]


class UserCreatedWebhook(WebhookModel[User]):
event: Literal["user.created"]


class UserDeletedWebhook(WebhookModel[User]):
event: Literal["user.deleted"]


class UserUpdatedWebhook(WebhookModel[User]):
event: Literal["user.updated"]


Webhook = Annotated[
Union[
AuthenticationEmailVerificationSucceededWebhook,
AuthenticationMagicAuthFailedWebhook,
AuthenticationMagicAuthSucceededWebhook,
AuthenticationMfaSucceededWebhook,
AuthenticationOauthSucceededWebhook,
AuthenticationPasswordFailedWebhook,
AuthenticationPasswordSucceededWebhook,
AuthenticationSsoSucceededWebhook,
ConnectionActivatedWebhook,
ConnectionDeactivatedWebhook,
ConnectionDeletedWebhook,
DirectoryActivatedWebhook,
DirectoryDeletedWebhook,
DirectoryGroupCreatedWebhook,
DirectoryGroupDeletedWebhook,
DirectoryGroupUpdatedWebhook,
DirectoryUserCreatedWebhook,
DirectoryUserDeletedWebhook,
DirectoryUserUpdatedWebhook,
DirectoryUserAddedToGroupWebhook,
DirectoryUserRemovedFromGroupWebhook,
EmailVerificationCreatedWebhook,
InvitationCreatedWebhook,
MagicAuthCreatedWebhook,
OrganizationCreatedWebhook,
OrganizationDeletedWebhook,
OrganizationUpdatedWebhook,
OrganizationDomainVerificationFailedWebhook,
OrganizationDomainVerifiedWebhook,
PasswordResetCreatedWebhook,
RoleCreatedWebhook,
RoleDeletedWebhook,
RoleUpdatedWebhook,
SessionCreatedWebhook,
UserCreatedWebhook,
UserDeletedWebhook,
UserUpdatedWebhook,
],
Field(..., discriminator="event"),
]
Loading
Loading