diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py index 004d6077..f403d9a1 100644 --- a/src/workos/types/events/__init__.py +++ b/src/workos/types/events/__init__.py @@ -13,3 +13,4 @@ from .organization_domain_verification_failed_payload import * from .previous_attributes import * from .session_payload import * +from .vault_payload import * diff --git a/src/workos/types/events/event.py b/src/workos/types/events/event.py index 028e08f3..8b72c967 100644 --- a/src/workos/types/events/event.py +++ b/src/workos/types/events/event.py @@ -53,6 +53,17 @@ SessionCreatedPayload, SessionRevokedPayload, ) +from workos.types.events.vault_payload import ( + VaultDataCreatedPayload, + VaultDataDeletedPayload, + VaultDataReadPayload, + VaultDataUpdatedPayload, + VaultDekDecryptedPayload, + VaultDekReadPayload, + VaultKekCreatedPayload, + VaultMetadataReadPayload, + VaultNamesListedPayload, +) from workos.types.organizations.organization_common import OrganizationCommon from workos.types.organization_domains import OrganizationDomain from workos.types.roles.role import EventRole @@ -372,6 +383,42 @@ class UserUpdatedEvent(EventModel[User]): event: Literal["user.updated"] +class VaultDataCreatedEvent(EventModel[VaultDataCreatedPayload]): + event: Literal["vault.data.created"] + + +class VaultDataDeletedEvent(EventModel[VaultDataDeletedPayload]): + event: Literal["vault.data.deleted"] + + +class VaultDataReadEvent(EventModel[VaultDataReadPayload]): + event: Literal["vault.data.read"] + + +class VaultDataUpdatedEvent(EventModel[VaultDataUpdatedPayload]): + event: Literal["vault.data.updated"] + + +class VaultDekDecryptedEvent(EventModel[VaultDekDecryptedPayload]): + event: Literal["vault.dek.decrypted"] + + +class VaultDekReadEvent(EventModel[VaultDekReadPayload]): + event: Literal["vault.dek.read"] + + +class VaultKekCreatedEvent(EventModel[VaultKekCreatedPayload]): + event: Literal["vault.kek.created"] + + +class VaultMetadataReadEvent(EventModel[VaultMetadataReadPayload]): + event: Literal["vault.metadata.read"] + + +class VaultNamesListedEvent(EventModel[VaultNamesListedPayload]): + event: Literal["vault.names.listed"] + + Event = Annotated[ Union[ ApiKeyCreatedEvent, @@ -443,6 +490,15 @@ class UserUpdatedEvent(EventModel[User]): UserCreatedEvent, UserDeletedEvent, UserUpdatedEvent, + VaultDataCreatedEvent, + VaultDataDeletedEvent, + VaultDataReadEvent, + VaultDataUpdatedEvent, + VaultDekDecryptedEvent, + VaultDekReadEvent, + VaultKekCreatedEvent, + VaultMetadataReadEvent, + VaultNamesListedEvent, ], Field(..., discriminator="event"), ] diff --git a/src/workos/types/events/event_model.py b/src/workos/types/events/event_model.py index 2d4fb4dc..43301891 100644 --- a/src/workos/types/events/event_model.py +++ b/src/workos/types/events/event_model.py @@ -50,6 +50,15 @@ SessionCreatedPayload, SessionRevokedPayload, ) +from workos.types.events.vault_payload import ( + VaultDataCreatedPayload, + VaultDataDeletedPayload, + VaultDataReadPayload, + VaultDekDecryptedPayload, + VaultDekReadPayload, + VaultKekCreatedPayload, + VaultNamesListedPayload, +) from workos.types.organizations.organization_common import OrganizationCommon from workos.types.organization_domains import OrganizationDomain from workos.types.authorization.organization_role import OrganizationRoleEvent @@ -110,6 +119,13 @@ SessionCreatedPayload, SessionRevokedPayload, User, + VaultDataCreatedPayload, + VaultDataDeletedPayload, + VaultDataReadPayload, + VaultDekDecryptedPayload, + VaultDekReadPayload, + VaultKekCreatedPayload, + VaultNamesListedPayload, ) diff --git a/src/workos/types/events/event_type.py b/src/workos/types/events/event_type.py index 6ccc5d57..b657158f 100644 --- a/src/workos/types/events/event_type.py +++ b/src/workos/types/events/event_type.py @@ -74,6 +74,15 @@ "user.created", "user.deleted", "user.updated", + "vault.data.created", + "vault.data.deleted", + "vault.data.read", + "vault.data.updated", + "vault.dek.decrypted", + "vault.dek.read", + "vault.kek.created", + "vault.metadata.read", + "vault.names.listed", ] EventTypeDiscriminator = TypeVar("EventTypeDiscriminator", bound=EventType) diff --git a/src/workos/types/events/vault_payload.py b/src/workos/types/events/vault_payload.py new file mode 100644 index 00000000..79e81bd4 --- /dev/null +++ b/src/workos/types/events/vault_payload.py @@ -0,0 +1,63 @@ +from typing import List, Optional + +from workos.types.vault.key import KeyContext +from workos.types.workos_model import WorkOSModel + + +class VaultNamesListedPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + + +class VaultDataDeletedPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + kv_name: str + + +class VaultDekDecryptedPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + key_id: str + + +class VaultDataReadPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + kv_name: str + key_id: str + + +class VaultDataCreatedPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + kv_name: str + key_id: str + key_context: Optional[KeyContext] = None + + +class VaultDekReadPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + key_ids: List[str] + key_context: Optional[KeyContext] = None + + +class VaultKekCreatedPayload(WorkOSModel): + actor_id: str + actor_source: str + actor_name: str + key_name: str + key_id: str + + +# Type aliases for events that reuse another event's payload shape. +# These give the .data field a semantically correct name for consumers. +VaultDataUpdatedPayload = VaultDataCreatedPayload +VaultMetadataReadPayload = VaultDataDeletedPayload diff --git a/src/workos/types/organizations/organization.py b/src/workos/types/organizations/organization.py index d7ba0aa4..e6c35922 100644 --- a/src/workos/types/organizations/organization.py +++ b/src/workos/types/organizations/organization.py @@ -9,5 +9,4 @@ class Organization(OrganizationCommon): allow_profiles_outside_organization: bool domains: Sequence[OrganizationDomain] stripe_customer_id: Optional[str] = None - external_id: Optional[str] = None metadata: Metadata = field(default_factory=dict) diff --git a/src/workos/types/organizations/organization_common.py b/src/workos/types/organizations/organization_common.py index 10a4c5c2..b3ab61e2 100644 --- a/src/workos/types/organizations/organization_common.py +++ b/src/workos/types/organizations/organization_common.py @@ -1,4 +1,4 @@ -from typing import Literal, Sequence +from typing import Literal, Optional, Sequence from workos.types.workos_model import WorkOSModel from workos.types.organization_domains import OrganizationDomain @@ -7,6 +7,7 @@ class OrganizationCommon(WorkOSModel): id: str object: Literal["organization"] name: str + external_id: Optional[str] = None domains: Sequence[OrganizationDomain] created_at: str updated_at: str diff --git a/src/workos/types/roles/role.py b/src/workos/types/roles/role.py index 7e299bd6..d65b9b62 100644 --- a/src/workos/types/roles/role.py +++ b/src/workos/types/roles/role.py @@ -11,6 +11,8 @@ class RoleCommon(WorkOSModel): class EventRole(RoleCommon): permissions: Optional[Sequence[str]] = None + created_at: Optional[str] = None + updated_at: Optional[str] = None class Role(RoleCommon): diff --git a/tests/test_events.py b/tests/test_events.py index 9a830557..42432634 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -5,6 +5,17 @@ from tests.utils.syncify import syncify from workos.events import AsyncEvents, Events, EventsListResource from workos.types.events import OrganizationMembershipCreatedEvent +from workos.types.events.event import ( + VaultDataCreatedEvent, + VaultDataDeletedEvent, + VaultDataReadEvent, + VaultDataUpdatedEvent, + VaultDekDecryptedEvent, + VaultDekReadEvent, + VaultKekCreatedEvent, + VaultMetadataReadEvent, + VaultNamesListedEvent, +) @pytest.mark.sync_and_async(Events, AsyncEvents) @@ -86,3 +97,371 @@ def test_list_events_organization_membership_missing_custom_attributes( event = events.data[0] assert isinstance(event, OrganizationMembershipCreatedEvent) assert event.data.custom_attributes == {} + + def test_list_events_vault_data_created( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_01", + "event": "vault.data.created", + "data": { + "actor_id": "user_01234", + "actor_source": "dashboard", + "actor_name": "Test User", + "kv_name": "my-secret", + "key_id": "key_01234", + "key_context": {"env": "production"}, + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": { + "after": None, + }, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.data.created"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDataCreatedEvent) + assert event.data.actor_id == "user_01234" + assert event.data.actor_source == "dashboard" + assert event.data.actor_name == "Test User" + assert event.data.kv_name == "my-secret" + assert event.data.key_id == "key_01234" + assert event.data.key_context.root == {"env": "production"} + + def test_list_events_vault_dek_read( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_02", + "event": "vault.dek.read", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "API Client", + "key_ids": ["key_01", "key_02"], + "key_context": {"tenant": "acme"}, + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": { + "after": None, + }, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.dek.read"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDekReadEvent) + assert event.data.key_ids == ["key_01", "key_02"] + assert event.data.key_context is not None + assert event.data.key_context.root == {"tenant": "acme"} + assert event.data.actor_name == "API Client" + + def test_list_events_vault_names_listed( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_03", + "event": "vault.names.listed", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Service Account", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": { + "after": None, + }, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.names.listed"]) + ) + + event = events.data[0] + assert isinstance(event, VaultNamesListedEvent) + assert event.data.actor_id == "user_01234" + assert event.data.actor_source == "api" + assert event.data.actor_name == "Service Account" + + def test_list_events_vault_data_read( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_09", + "event": "vault.data.read", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Read Service", + "kv_name": "db-password", + "key_id": "key_55", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.data.read"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDataReadEvent) + assert event.data.kv_name == "db-password" + assert event.data.key_id == "key_55" + + def test_list_events_vault_dek_decrypted( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_04", + "event": "vault.dek.decrypted", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Decryption Service", + "key_id": "key_99", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.dek.decrypted"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDekDecryptedEvent) + assert event.data.key_id == "key_99" + assert event.data.actor_name == "Decryption Service" + + def test_list_events_vault_kek_created( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_05", + "event": "vault.kek.created", + "data": { + "actor_id": "user_01234", + "actor_source": "dashboard", + "actor_name": "Admin", + "key_name": "production-kek", + "key_id": "kek_01", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.kek.created"]) + ) + + event = events.data[0] + assert isinstance(event, VaultKekCreatedEvent) + assert event.data.key_name == "production-kek" + assert event.data.key_id == "kek_01" + + def test_list_events_vault_data_deleted( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_06", + "event": "vault.data.deleted", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Cleanup Job", + "kv_name": "old-secret", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.data.deleted"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDataDeletedEvent) + assert event.data.kv_name == "old-secret" + + def test_list_events_vault_data_updated( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_07", + "event": "vault.data.updated", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Rotation Job", + "kv_name": "api-key", + "key_id": "key_02", + "key_context": {"env": "staging"}, + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.data.updated"]) + ) + + event = events.data[0] + assert isinstance(event, VaultDataUpdatedEvent) + assert event.data.kv_name == "api-key" + assert event.data.key_id == "key_02" + + def test_list_events_vault_metadata_read( + self, + module_instance: Union[Events, AsyncEvents], + capture_and_mock_http_client_request, + ): + mock_response = { + "object": "list", + "data": [ + { + "object": "event", + "id": "event_vault_08", + "event": "vault.metadata.read", + "data": { + "actor_id": "user_01234", + "actor_source": "api", + "actor_name": "Audit Service", + "kv_name": "config-store", + }, + "created_at": "2024-01-01T00:00:00.000Z", + } + ], + "list_metadata": {"after": None}, + } + + capture_and_mock_http_client_request( + http_client=module_instance._http_client, + status_code=200, + response_dict=mock_response, + ) + + events: EventsListResource = syncify( + module_instance.list_events(events=["vault.metadata.read"]) + ) + + event = events.data[0] + assert isinstance(event, VaultMetadataReadEvent) + assert event.data.kv_name == "config-store"