workos 5.23.0__tar.gz → 5.26.0__tar.gz

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.
Files changed (157) hide show
  1. {workos-5.23.0 → workos-5.26.0}/PKG-INFO +1 -1
  2. workos-5.26.0/tests/test_organization_domains.py +136 -0
  3. {workos-5.23.0 → workos-5.26.0}/tests/test_organizations.py +13 -9
  4. workos-5.26.0/tests/test_vault.py +460 -0
  5. {workos-5.23.0 → workos-5.26.0}/workos/__about__.py +1 -1
  6. {workos-5.23.0 → workos-5.26.0}/workos/_base_client.py +5 -0
  7. {workos-5.23.0 → workos-5.26.0}/workos/async_client.py +16 -0
  8. {workos-5.23.0 → workos-5.26.0}/workos/client.py +16 -0
  9. workos-5.26.0/workos/organization_domains.py +179 -0
  10. {workos-5.23.0 → workos-5.26.0}/workos/types/events/event.py +16 -1
  11. {workos-5.23.0 → workos-5.26.0}/workos/types/events/event_model.py +1 -1
  12. {workos-5.23.0 → workos-5.26.0}/workos/types/events/event_type.py +3 -0
  13. {workos-5.23.0 → workos-5.26.0}/workos/types/events/organization_domain_verification_failed_payload.py +1 -1
  14. {workos-5.23.0 → workos-5.26.0}/workos/types/list_resource.py +2 -0
  15. workos-5.26.0/workos/types/organization_domains/__init__.py +1 -0
  16. {workos-5.23.0/workos/types/organizations → workos-5.26.0/workos/types/organization_domains}/organization_domain.py +3 -0
  17. workos-5.26.0/workos/types/organizations/__init__.py +6 -0
  18. {workos-5.23.0 → workos-5.26.0}/workos/types/organizations/organization.py +1 -1
  19. {workos-5.23.0 → workos-5.26.0}/workos/types/organizations/organization_common.py +1 -1
  20. workos-5.26.0/workos/types/vault/__init__.py +2 -0
  21. workos-5.26.0/workos/types/vault/key.py +25 -0
  22. workos-5.26.0/workos/types/vault/object.py +38 -0
  23. {workos-5.23.0 → workos-5.26.0}/workos/types/webhooks/webhook.py +1 -1
  24. workos-5.26.0/workos/utils/crypto_provider.py +39 -0
  25. workos-5.26.0/workos/vault.py +515 -0
  26. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/PKG-INFO +1 -1
  27. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/SOURCES.txt +10 -1
  28. workos-5.23.0/workos/types/organizations/__init__.py +0 -4
  29. {workos-5.23.0 → workos-5.26.0}/LICENSE +0 -0
  30. {workos-5.23.0 → workos-5.26.0}/README.md +0 -0
  31. {workos-5.23.0 → workos-5.26.0}/setup.cfg +0 -0
  32. {workos-5.23.0 → workos-5.26.0}/setup.py +0 -0
  33. {workos-5.23.0 → workos-5.26.0}/tests/test_async_http_client.py +0 -0
  34. {workos-5.23.0 → workos-5.26.0}/tests/test_audit_logs.py +0 -0
  35. {workos-5.23.0 → workos-5.26.0}/tests/test_client.py +0 -0
  36. {workos-5.23.0 → workos-5.26.0}/tests/test_directory_sync.py +0 -0
  37. {workos-5.23.0 → workos-5.26.0}/tests/test_events.py +0 -0
  38. {workos-5.23.0 → workos-5.26.0}/tests/test_fga.py +0 -0
  39. {workos-5.23.0 → workos-5.26.0}/tests/test_mfa.py +0 -0
  40. {workos-5.23.0 → workos-5.26.0}/tests/test_passwordless.py +0 -0
  41. {workos-5.23.0 → workos-5.26.0}/tests/test_portal.py +0 -0
  42. {workos-5.23.0 → workos-5.26.0}/tests/test_session.py +0 -0
  43. {workos-5.23.0 → workos-5.26.0}/tests/test_sso.py +0 -0
  44. {workos-5.23.0 → workos-5.26.0}/tests/test_sync_http_client.py +0 -0
  45. {workos-5.23.0 → workos-5.26.0}/tests/test_user_management.py +0 -0
  46. {workos-5.23.0 → workos-5.26.0}/tests/test_webhooks.py +0 -0
  47. {workos-5.23.0 → workos-5.26.0}/tests/test_widgets.py +0 -0
  48. {workos-5.23.0 → workos-5.26.0}/workos/__init__.py +0 -0
  49. {workos-5.23.0 → workos-5.26.0}/workos/_client_configuration.py +0 -0
  50. {workos-5.23.0 → workos-5.26.0}/workos/audit_logs.py +0 -0
  51. {workos-5.23.0 → workos-5.26.0}/workos/directory_sync.py +0 -0
  52. {workos-5.23.0 → workos-5.26.0}/workos/events.py +0 -0
  53. {workos-5.23.0 → workos-5.26.0}/workos/exceptions.py +0 -0
  54. {workos-5.23.0 → workos-5.26.0}/workos/fga.py +0 -0
  55. {workos-5.23.0 → workos-5.26.0}/workos/mfa.py +0 -0
  56. {workos-5.23.0 → workos-5.26.0}/workos/organizations.py +0 -0
  57. {workos-5.23.0 → workos-5.26.0}/workos/passwordless.py +0 -0
  58. {workos-5.23.0 → workos-5.26.0}/workos/portal.py +0 -0
  59. {workos-5.23.0 → workos-5.26.0}/workos/py.typed +0 -0
  60. {workos-5.23.0 → workos-5.26.0}/workos/session.py +0 -0
  61. {workos-5.23.0 → workos-5.26.0}/workos/sso.py +0 -0
  62. {workos-5.23.0 → workos-5.26.0}/workos/types/__init__.py +0 -0
  63. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/__init__.py +0 -0
  64. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_event.py +0 -0
  65. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_event_actor.py +0 -0
  66. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_event_context.py +0 -0
  67. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_event_target.py +0 -0
  68. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_export.py +0 -0
  69. {workos-5.23.0 → workos-5.26.0}/workos/types/audit_logs/audit_log_metadata.py +0 -0
  70. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/__init__.py +0 -0
  71. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/directory.py +0 -0
  72. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/directory_group.py +0 -0
  73. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/directory_state.py +0 -0
  74. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/directory_type.py +0 -0
  75. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/directory_user.py +0 -0
  76. {workos-5.23.0 → workos-5.26.0}/workos/types/directory_sync/list_filters.py +0 -0
  77. {workos-5.23.0 → workos-5.26.0}/workos/types/events/__init__.py +0 -0
  78. {workos-5.23.0 → workos-5.26.0}/workos/types/events/authentication_payload.py +0 -0
  79. {workos-5.23.0 → workos-5.26.0}/workos/types/events/connection_payload_with_legacy_fields.py +0 -0
  80. {workos-5.23.0 → workos-5.26.0}/workos/types/events/directory_group_membership_payload.py +0 -0
  81. {workos-5.23.0 → workos-5.26.0}/workos/types/events/directory_group_with_previous_attributes.py +0 -0
  82. {workos-5.23.0 → workos-5.26.0}/workos/types/events/directory_payload.py +0 -0
  83. {workos-5.23.0 → workos-5.26.0}/workos/types/events/directory_payload_with_legacy_fields.py +0 -0
  84. {workos-5.23.0 → workos-5.26.0}/workos/types/events/directory_user_with_previous_attributes.py +0 -0
  85. {workos-5.23.0 → workos-5.26.0}/workos/types/events/list_filters.py +0 -0
  86. {workos-5.23.0 → workos-5.26.0}/workos/types/events/previous_attributes.py +0 -0
  87. {workos-5.23.0 → workos-5.26.0}/workos/types/events/session_created_payload.py +0 -0
  88. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/__init__.py +0 -0
  89. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/authorization_resource_types.py +0 -0
  90. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/authorization_resources.py +0 -0
  91. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/check.py +0 -0
  92. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/list_filters.py +0 -0
  93. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/warnings.py +0 -0
  94. {workos-5.23.0 → workos-5.26.0}/workos/types/fga/warrant.py +0 -0
  95. {workos-5.23.0 → workos-5.26.0}/workos/types/metadata.py +0 -0
  96. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/__init__.py +0 -0
  97. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/authentication_challenge.py +0 -0
  98. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/authentication_challenge_verification_response.py +0 -0
  99. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/authentication_factor.py +0 -0
  100. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/authentication_factor_totp_and_challenge_response.py +0 -0
  101. {workos-5.23.0 → workos-5.26.0}/workos/types/mfa/enroll_authentication_factor_type.py +0 -0
  102. {workos-5.23.0 → workos-5.26.0}/workos/types/organizations/domain_data_input.py +0 -0
  103. {workos-5.23.0 → workos-5.26.0}/workos/types/organizations/list_filters.py +0 -0
  104. {workos-5.23.0 → workos-5.26.0}/workos/types/passwordless/__init__.py +0 -0
  105. {workos-5.23.0 → workos-5.26.0}/workos/types/passwordless/passwordless_session.py +0 -0
  106. {workos-5.23.0 → workos-5.26.0}/workos/types/passwordless/passwordless_session_type.py +0 -0
  107. {workos-5.23.0 → workos-5.26.0}/workos/types/portal/__init__.py +0 -0
  108. {workos-5.23.0 → workos-5.26.0}/workos/types/portal/portal_link.py +0 -0
  109. {workos-5.23.0 → workos-5.26.0}/workos/types/portal/portal_link_intent.py +0 -0
  110. {workos-5.23.0 → workos-5.26.0}/workos/types/portal/portal_link_intent_options.py +0 -0
  111. {workos-5.23.0 → workos-5.26.0}/workos/types/roles/__init__.py +0 -0
  112. {workos-5.23.0 → workos-5.26.0}/workos/types/roles/role.py +0 -0
  113. {workos-5.23.0 → workos-5.26.0}/workos/types/sso/__init__.py +0 -0
  114. {workos-5.23.0 → workos-5.26.0}/workos/types/sso/connection.py +0 -0
  115. {workos-5.23.0 → workos-5.26.0}/workos/types/sso/connection_domain.py +0 -0
  116. {workos-5.23.0 → workos-5.26.0}/workos/types/sso/profile.py +0 -0
  117. {workos-5.23.0 → workos-5.26.0}/workos/types/sso/sso_provider_type.py +0 -0
  118. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/__init__.py +0 -0
  119. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/authenticate_with_common.py +0 -0
  120. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/authentication_response.py +0 -0
  121. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/email_verification.py +0 -0
  122. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/impersonator.py +0 -0
  123. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/invitation.py +0 -0
  124. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/list_filters.py +0 -0
  125. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/magic_auth.py +0 -0
  126. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/oauth_tokens.py +0 -0
  127. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/organization_membership.py +0 -0
  128. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/password_hash_type.py +0 -0
  129. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/password_reset.py +0 -0
  130. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/screen_hint.py +0 -0
  131. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/session.py +0 -0
  132. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/user.py +0 -0
  133. {workos-5.23.0 → workos-5.26.0}/workos/types/user_management/user_management_provider_type.py +0 -0
  134. {workos-5.23.0 → workos-5.26.0}/workos/types/webhooks/__init__.py +0 -0
  135. {workos-5.23.0 → workos-5.26.0}/workos/types/webhooks/webhook_model.py +0 -0
  136. {workos-5.23.0 → workos-5.26.0}/workos/types/webhooks/webhook_payload.py +0 -0
  137. {workos-5.23.0 → workos-5.26.0}/workos/types/widgets/__init__.py +0 -0
  138. {workos-5.23.0 → workos-5.26.0}/workos/types/widgets/widget_scope.py +0 -0
  139. {workos-5.23.0 → workos-5.26.0}/workos/types/widgets/widget_token_response.py +0 -0
  140. {workos-5.23.0 → workos-5.26.0}/workos/types/workos_model.py +0 -0
  141. {workos-5.23.0 → workos-5.26.0}/workos/typing/__init__.py +0 -0
  142. {workos-5.23.0 → workos-5.26.0}/workos/typing/literals.py +0 -0
  143. {workos-5.23.0 → workos-5.26.0}/workos/typing/sync_or_async.py +0 -0
  144. {workos-5.23.0 → workos-5.26.0}/workos/typing/untyped_literal.py +0 -0
  145. {workos-5.23.0 → workos-5.26.0}/workos/typing/webhooks.py +0 -0
  146. {workos-5.23.0 → workos-5.26.0}/workos/user_management.py +0 -0
  147. {workos-5.23.0 → workos-5.26.0}/workos/utils/__init__.py +0 -0
  148. {workos-5.23.0 → workos-5.26.0}/workos/utils/_base_http_client.py +0 -0
  149. {workos-5.23.0 → workos-5.26.0}/workos/utils/http_client.py +0 -0
  150. {workos-5.23.0 → workos-5.26.0}/workos/utils/pagination_order.py +0 -0
  151. {workos-5.23.0 → workos-5.26.0}/workos/utils/request_helper.py +0 -0
  152. {workos-5.23.0 → workos-5.26.0}/workos/webhooks.py +0 -0
  153. {workos-5.23.0 → workos-5.26.0}/workos/widgets.py +0 -0
  154. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/dependency_links.txt +0 -0
  155. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/not-zip-safe +0 -0
  156. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/requires.txt +0 -0
  157. {workos-5.23.0 → workos-5.26.0}/workos.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: workos
3
- Version: 5.23.0
3
+ Version: 5.26.0
4
4
  Summary: WorkOS Python Client
5
5
  Home-page: https://github.com/workos-inc/workos-python
6
6
  Author: WorkOS
@@ -0,0 +1,136 @@
1
+ from typing import Union
2
+ import pytest
3
+ from tests.utils.syncify import syncify
4
+ from workos.organization_domains import AsyncOrganizationDomains, OrganizationDomains
5
+
6
+
7
+ @pytest.mark.sync_and_async(OrganizationDomains, AsyncOrganizationDomains)
8
+ class TestOrganizationDomains:
9
+ @pytest.fixture(autouse=True)
10
+ def setup(
11
+ self, module_instance: Union[OrganizationDomains, AsyncOrganizationDomains]
12
+ ):
13
+ self.http_client = module_instance._http_client
14
+ self.organization_domains = module_instance
15
+
16
+ @pytest.fixture
17
+ def mock_organization_domain(self):
18
+ return {
19
+ "object": "organization_domain",
20
+ "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
21
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
22
+ "domain": "example.com",
23
+ "state": "pending",
24
+ "verification_strategy": "dns",
25
+ "verification_token": "workos_example_verification_token_12345",
26
+ "verification_prefix": "_workos-challenge",
27
+ "created_at": "2023-01-01T12:00:00.000Z",
28
+ "updated_at": "2023-01-01T12:00:00.000Z",
29
+ }
30
+
31
+ @pytest.fixture
32
+ def mock_organization_domain_verified(self):
33
+ return {
34
+ "object": "organization_domain",
35
+ "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
36
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
37
+ "domain": "example.com",
38
+ "state": "verified",
39
+ "verification_strategy": "dns",
40
+ "verification_token": "workos_example_verification_token_12345",
41
+ "verification_prefix": "_workos-challenge",
42
+ "created_at": "2023-01-01T12:00:00.000Z",
43
+ "updated_at": "2023-01-01T12:00:00.000Z",
44
+ }
45
+
46
+ def test_get_organization_domain(
47
+ self, capture_and_mock_http_client_request, mock_organization_domain
48
+ ):
49
+ request_kwargs = capture_and_mock_http_client_request(
50
+ self.http_client,
51
+ mock_organization_domain,
52
+ 200,
53
+ )
54
+
55
+ organization_domain = syncify(
56
+ self.organization_domains.get_organization_domain(
57
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
58
+ )
59
+ )
60
+
61
+ assert request_kwargs["url"].endswith(
62
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
63
+ )
64
+ assert request_kwargs["method"] == "get"
65
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
66
+ assert organization_domain.domain == "example.com"
67
+ assert organization_domain.state == "pending"
68
+ assert organization_domain.verification_strategy == "dns"
69
+
70
+ def test_create_organization_domain(
71
+ self, capture_and_mock_http_client_request, mock_organization_domain
72
+ ):
73
+ request_kwargs = capture_and_mock_http_client_request(
74
+ self.http_client,
75
+ mock_organization_domain,
76
+ 201,
77
+ )
78
+
79
+ organization_domain = syncify(
80
+ self.organization_domains.create_organization_domain(
81
+ organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T",
82
+ domain="example.com",
83
+ )
84
+ )
85
+
86
+ assert request_kwargs["url"].endswith("/organization_domains")
87
+ assert request_kwargs["method"] == "post"
88
+ assert request_kwargs["json"] == {
89
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
90
+ "domain": "example.com",
91
+ }
92
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
93
+ assert organization_domain.domain == "example.com"
94
+ assert organization_domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
95
+
96
+ def test_verify_organization_domain(
97
+ self, capture_and_mock_http_client_request, mock_organization_domain_verified
98
+ ):
99
+ request_kwargs = capture_and_mock_http_client_request(
100
+ self.http_client,
101
+ mock_organization_domain_verified,
102
+ 200,
103
+ )
104
+
105
+ organization_domain = syncify(
106
+ self.organization_domains.verify_organization_domain(
107
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
108
+ )
109
+ )
110
+
111
+ assert request_kwargs["url"].endswith(
112
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8/verify"
113
+ )
114
+ assert request_kwargs["method"] == "post"
115
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
116
+ assert organization_domain.state == "verified"
117
+
118
+ def test_delete_organization_domain(self, capture_and_mock_http_client_request):
119
+ request_kwargs = capture_and_mock_http_client_request(
120
+ self.http_client,
121
+ None,
122
+ 204,
123
+ headers={"content-type": "text/plain; charset=utf-8"},
124
+ )
125
+
126
+ response = syncify(
127
+ self.organization_domains.delete_organization_domain(
128
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
129
+ )
130
+ )
131
+
132
+ assert request_kwargs["url"].endswith(
133
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
134
+ )
135
+ assert request_kwargs["method"] == "delete"
136
+ assert response is None
@@ -38,6 +38,8 @@ class TestOrganizations:
38
38
  "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
39
39
  "verification_strategy": "dns",
40
40
  "verification_token": "token",
41
+ "created_at": datetime.datetime.now().isoformat(),
42
+ "updated_at": datetime.datetime.now().isoformat(),
41
43
  }
42
44
  ],
43
45
  }
@@ -189,15 +191,17 @@ class TestOrganizations:
189
191
  }
190
192
  assert updated_organization.id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
191
193
  assert updated_organization.name == "Example Organization"
192
- assert updated_organization.domains[0].dict() == {
193
- "domain": "example.io",
194
- "object": "organization_domain",
195
- "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
196
- "state": "verified",
197
- "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
198
- "verification_strategy": "dns",
199
- "verification_token": "token",
200
- }
194
+ domain = updated_organization.domains[0]
195
+ assert domain.domain == "example.io"
196
+ assert domain.object == "organization_domain"
197
+ assert domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
198
+ assert domain.state == "verified"
199
+ assert domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
200
+ assert domain.verification_strategy == "dns"
201
+ assert domain.verification_token == "token"
202
+ assert domain.verification_prefix is None
203
+ assert isinstance(domain.created_at, str)
204
+ assert isinstance(domain.updated_at, str)
201
205
 
202
206
  def test_delete_organization(self, capture_and_mock_http_client_request):
203
207
  request_kwargs = capture_and_mock_http_client_request(
@@ -0,0 +1,460 @@
1
+ import pytest
2
+ from tests.utils.fixtures.mock_vault_object import (
3
+ MockVaultObject,
4
+ MockObjectVersion,
5
+ MockObjectDigest,
6
+ MockObjectMetadata,
7
+ )
8
+ from tests.utils.list_resource import list_response_of
9
+ from tests.utils.syncify import syncify
10
+ from workos.vault import Vault
11
+ from workos.types.vault.key import KeyContext
12
+
13
+
14
+ class TestVault:
15
+ @pytest.fixture(autouse=True)
16
+ def setup(self, sync_http_client_for_test):
17
+ self.http_client = sync_http_client_for_test
18
+ self.vault = Vault(http_client=self.http_client)
19
+
20
+ @pytest.fixture
21
+ def mock_vault_object(self):
22
+ return MockVaultObject(
23
+ "vault_01234567890abcdef", "test-secret", "secret-value"
24
+ ).dict()
25
+
26
+ @pytest.fixture
27
+ def mock_object_digest(self):
28
+ return MockObjectDigest("vault_01234567890abcdef", "test-secret").dict()
29
+
30
+ @pytest.fixture
31
+ def mock_object_metadata(self):
32
+ return MockObjectMetadata("vault_01234567890abcdef").dict()
33
+
34
+ @pytest.fixture
35
+ def mock_vault_object_no_value(self):
36
+ mock_obj = MockVaultObject("vault_01234567890abcdef", "test-secret")
37
+ mock_obj.value = None
38
+ return mock_obj.dict()
39
+
40
+ @pytest.fixture
41
+ def mock_vault_objects_list(self):
42
+ vault_objects = [
43
+ MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(5)
44
+ ]
45
+ return {
46
+ "data": vault_objects,
47
+ "list_metadata": {"before": None, "after": None},
48
+ "object": "list",
49
+ }
50
+
51
+ @pytest.fixture
52
+ def mock_vault_objects_multiple_pages(self):
53
+ vault_objects = [
54
+ MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(25)
55
+ ]
56
+ return list_response_of(data=vault_objects)
57
+
58
+ @pytest.fixture
59
+ def mock_object_versions(self):
60
+ versions = [
61
+ MockObjectVersion(f"version_{i}", current_version=(i == 0)).dict()
62
+ for i in range(3)
63
+ ]
64
+ return {"data": versions}
65
+
66
+ @pytest.fixture
67
+ def mock_data_key(self):
68
+ return {
69
+ "id": "key_01234567890abcdef",
70
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
71
+ }
72
+
73
+ @pytest.fixture
74
+ def mock_data_key_pair(self):
75
+ return {
76
+ "context": {"key": "test-key"},
77
+ "id": "key_01234567890abcdef",
78
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
79
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
80
+ }
81
+
82
+ def test_read_object_success(
83
+ self, mock_vault_object, capture_and_mock_http_client_request
84
+ ):
85
+ request_kwargs = capture_and_mock_http_client_request(
86
+ self.http_client, mock_vault_object, 200
87
+ )
88
+
89
+ vault_object = self.vault.read_object(object_id="vault_01234567890abcdef")
90
+
91
+ assert request_kwargs["method"] == "get"
92
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
93
+ assert vault_object.id == "vault_01234567890abcdef"
94
+ assert vault_object.name == "test-secret"
95
+ assert vault_object.value == "secret-value"
96
+ assert vault_object.metadata.environment_id == "env_01234567890abcdef"
97
+
98
+ def test_read_object_missing_object_id(self):
99
+ with pytest.raises(
100
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
101
+ ):
102
+ self.vault.read_object(object_id="")
103
+
104
+ def test_read_object_none_object_id(self):
105
+ with pytest.raises(
106
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
107
+ ):
108
+ self.vault.read_object(object_id=None)
109
+
110
+ def test_list_objects_default_params(
111
+ self, mock_vault_objects_list, capture_and_mock_http_client_request
112
+ ):
113
+ request_kwargs = capture_and_mock_http_client_request(
114
+ self.http_client, mock_vault_objects_list, 200
115
+ )
116
+
117
+ vault_objects = self.vault.list_objects()
118
+
119
+ assert request_kwargs["method"] == "get"
120
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
121
+ assert request_kwargs["params"]["limit"] == 10
122
+ assert "before" not in request_kwargs["params"]
123
+ assert "after" not in request_kwargs["params"]
124
+ assert len(vault_objects.data) == 5
125
+ assert vault_objects.data[0].id == "vault_0"
126
+ assert vault_objects.data[0].name == "secret-0"
127
+
128
+ def test_list_objects_with_params(
129
+ self, mock_vault_objects_list, capture_and_mock_http_client_request
130
+ ):
131
+ request_kwargs = capture_and_mock_http_client_request(
132
+ self.http_client, mock_vault_objects_list, 200
133
+ )
134
+
135
+ vault_objects = self.vault.list_objects(
136
+ limit=5, before="vault_before", after="vault_after"
137
+ )
138
+
139
+ assert request_kwargs["method"] == "get"
140
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
141
+ assert request_kwargs["params"]["limit"] == 5
142
+ assert request_kwargs["params"]["before"] == "vault_before"
143
+ assert request_kwargs["params"]["after"] == "vault_after"
144
+
145
+ def test_list_objects_auto_pagination(
146
+ self, mock_vault_objects_multiple_pages, test_auto_pagination
147
+ ):
148
+ test_auto_pagination(
149
+ http_client=self.http_client,
150
+ list_function=self.vault.list_objects,
151
+ expected_all_page_data=mock_vault_objects_multiple_pages["data"],
152
+ )
153
+
154
+ def test_list_object_versions_success(
155
+ self, mock_object_versions, capture_and_mock_http_client_request
156
+ ):
157
+ request_kwargs = capture_and_mock_http_client_request(
158
+ self.http_client, mock_object_versions, 200
159
+ )
160
+
161
+ versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef")
162
+
163
+ assert request_kwargs["method"] == "get"
164
+ assert request_kwargs["url"].endswith(
165
+ "/vault/v1/kv/vault_01234567890abcdef/versions"
166
+ )
167
+ assert len(versions) == 3
168
+ assert versions[0].id == "version_0"
169
+ assert versions[0].current_version is True
170
+ assert versions[1].current_version is False
171
+
172
+ def test_list_object_versions_empty_data(
173
+ self, capture_and_mock_http_client_request
174
+ ):
175
+ request_kwargs = capture_and_mock_http_client_request(
176
+ self.http_client, {"data": []}, 200
177
+ )
178
+
179
+ versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef")
180
+
181
+ assert request_kwargs["method"] == "get"
182
+ assert len(versions) == 0
183
+
184
+ def test_create_object_success(
185
+ self, mock_object_metadata, capture_and_mock_http_client_request
186
+ ):
187
+ request_kwargs = capture_and_mock_http_client_request(
188
+ self.http_client, mock_object_metadata, 200
189
+ )
190
+
191
+ object_metadata = self.vault.create_object(
192
+ name="test-secret",
193
+ value="secret-value",
194
+ key_context=KeyContext({"key": "test-key"}),
195
+ )
196
+
197
+ assert request_kwargs["method"] == "post"
198
+ assert request_kwargs["url"].endswith("/vault/v1/kv")
199
+ assert request_kwargs["json"]["name"] == "test-secret"
200
+ assert request_kwargs["json"]["value"] == "secret-value"
201
+ assert request_kwargs["json"]["key_context"] == KeyContext({"key": "test-key"})
202
+ assert object_metadata.id == "vault_01234567890abcdef"
203
+
204
+ def test_create_object_missing_name(self):
205
+ with pytest.raises(
206
+ ValueError,
207
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
208
+ ):
209
+ self.vault.create_object(
210
+ name="",
211
+ value="secret-value",
212
+ key_context=KeyContext({"key": "test-key"}),
213
+ )
214
+
215
+ def test_create_object_missing_value(self):
216
+ with pytest.raises(
217
+ ValueError,
218
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
219
+ ):
220
+ self.vault.create_object(
221
+ name="test-secret",
222
+ value="",
223
+ key_context=KeyContext({"key": "test-key"}),
224
+ )
225
+
226
+ def test_create_object_missing_both(self):
227
+ with pytest.raises(
228
+ ValueError,
229
+ match="Incomplete arguments: 'name' and 'value' are required arguments",
230
+ ):
231
+ self.vault.create_object(
232
+ name="", value="", key_context=KeyContext({"key": "test-key"})
233
+ )
234
+
235
+ def test_update_object_with_value(
236
+ self, mock_vault_object, capture_and_mock_http_client_request
237
+ ):
238
+ request_kwargs = capture_and_mock_http_client_request(
239
+ self.http_client, mock_vault_object, 200
240
+ )
241
+
242
+ vault_object = self.vault.update_object(
243
+ object_id="vault_01234567890abcdef",
244
+ value="updated-value",
245
+ )
246
+
247
+ assert request_kwargs["method"] == "put"
248
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
249
+ assert request_kwargs["json"]["value"] == "updated-value"
250
+ assert "version_check" not in request_kwargs["json"]
251
+ assert vault_object.id == "vault_01234567890abcdef"
252
+
253
+ def test_update_object_with_version_check(
254
+ self, mock_vault_object, capture_and_mock_http_client_request
255
+ ):
256
+ request_kwargs = capture_and_mock_http_client_request(
257
+ self.http_client, mock_vault_object, 200
258
+ )
259
+
260
+ vault_object = self.vault.update_object(
261
+ object_id="vault_01234567890abcdef",
262
+ value="updated-value",
263
+ version_check="version_123",
264
+ )
265
+
266
+ assert request_kwargs["method"] == "put"
267
+ assert request_kwargs["json"]["value"] == "updated-value"
268
+ assert request_kwargs["json"]["version_check"] == "version_123"
269
+
270
+ def test_update_object_missing_value(self):
271
+ with pytest.raises(
272
+ TypeError, match="missing 1 required keyword-only argument: 'value'"
273
+ ):
274
+ self.vault.update_object(object_id="vault_01234567890abcdef")
275
+
276
+ def test_update_object_missing_object_id(self):
277
+ with pytest.raises(
278
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
279
+ ):
280
+ self.vault.update_object(object_id="", value="test-value")
281
+
282
+ def test_update_object_none_object_id(self):
283
+ with pytest.raises(
284
+ ValueError,
285
+ match="Incomplete arguments: 'object_id' is a required argument",
286
+ ):
287
+ self.vault.update_object(object_id=None, value="updated-value")
288
+
289
+ def test_delete_object_success(self, capture_and_mock_http_client_request):
290
+ request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 204)
291
+
292
+ result = self.vault.delete_object(object_id="vault_01234567890abcdef")
293
+
294
+ assert request_kwargs["method"] == "delete"
295
+ assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef")
296
+ assert result is None
297
+
298
+ def test_delete_object_missing_object_id(self):
299
+ with pytest.raises(
300
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
301
+ ):
302
+ self.vault.delete_object(object_id="")
303
+
304
+ def test_delete_object_none_object_id(self):
305
+ with pytest.raises(
306
+ ValueError, match="Incomplete arguments: 'object_id' is a required argument"
307
+ ):
308
+ self.vault.delete_object(object_id=None)
309
+
310
+ def test_create_data_key_success(
311
+ self, mock_data_key_pair, capture_and_mock_http_client_request
312
+ ):
313
+ request_kwargs = capture_and_mock_http_client_request(
314
+ self.http_client, mock_data_key_pair, 200
315
+ )
316
+
317
+ data_key_pair = self.vault.create_data_key(
318
+ key_context=KeyContext({"key": "test-key"})
319
+ )
320
+
321
+ assert request_kwargs["method"] == "post"
322
+ assert request_kwargs["url"].endswith("/vault/v1/keys/data-key")
323
+ assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"})
324
+ assert data_key_pair.data_key.id == "key_01234567890abcdef"
325
+ assert data_key_pair.encrypted_keys == "ZW5jcnlwdGVkX2tleXNfZGF0YQ=="
326
+
327
+ def test_decrypt_data_key_success(
328
+ self, mock_data_key, capture_and_mock_http_client_request
329
+ ):
330
+ request_kwargs = capture_and_mock_http_client_request(
331
+ self.http_client, mock_data_key, 200
332
+ )
333
+
334
+ data_key = self.vault.decrypt_data_key(keys="ZW5jcnlwdGVkX2tleXNfZGF0YQ==")
335
+
336
+ assert request_kwargs["method"] == "post"
337
+ assert request_kwargs["url"].endswith("/vault/v1/keys/decrypt")
338
+ assert request_kwargs["json"]["keys"] == "ZW5jcnlwdGVkX2tleXNfZGF0YQ=="
339
+ assert data_key.id == "key_01234567890abcdef"
340
+ assert data_key.key == "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
341
+
342
+ def test_encrypt_success(
343
+ self, mock_data_key_pair, capture_and_mock_http_client_request
344
+ ):
345
+ # Mock the create_data_key call
346
+ request_kwargs = capture_and_mock_http_client_request(
347
+ self.http_client, mock_data_key_pair, 200
348
+ )
349
+
350
+ plaintext = "Hello, World!"
351
+ context = KeyContext({"key": "test-key"})
352
+
353
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
354
+
355
+ # Verify create_data_key was called
356
+ assert request_kwargs["method"] == "post"
357
+ assert request_kwargs["url"].endswith("/vault/v1/keys/data-key")
358
+ assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"})
359
+
360
+ # Verify we got encrypted data back
361
+ assert isinstance(encrypted_data, str)
362
+ assert len(encrypted_data) > 0
363
+
364
+ def test_encrypt_with_associated_data(
365
+ self, mock_data_key_pair, capture_and_mock_http_client_request
366
+ ):
367
+ # Mock the create_data_key call
368
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
369
+
370
+ plaintext = "Hello, World!"
371
+ context = KeyContext({"key": "test-key"})
372
+ associated_data = "additional-context"
373
+
374
+ encrypted_data = self.vault.encrypt(
375
+ data=plaintext, key_context=context, associated_data=associated_data
376
+ )
377
+
378
+ # Verify we got encrypted data back
379
+ assert isinstance(encrypted_data, str)
380
+ assert len(encrypted_data) > 0
381
+
382
+ def test_decrypt_success(self, mock_data_key, capture_and_mock_http_client_request):
383
+ # First encrypt some data to get a valid encrypted payload
384
+ mock_data_key_pair = {
385
+ "context": {"key": "test-key"},
386
+ "id": "key_01234567890abcdef",
387
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
388
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
389
+ }
390
+
391
+ # Mock create_data_key for encryption
392
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
393
+
394
+ plaintext = "Hello, World!"
395
+ context = KeyContext({"key": "test-key"})
396
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
397
+
398
+ # Now mock decrypt_data_key for decryption
399
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
400
+
401
+ # Decrypt the data
402
+ decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data)
403
+
404
+ # Verify decryption worked
405
+ assert decrypted_text == plaintext
406
+
407
+ def test_decrypt_with_associated_data(
408
+ self, mock_data_key, capture_and_mock_http_client_request
409
+ ):
410
+ # First encrypt some data with associated data
411
+ mock_data_key_pair = {
412
+ "context": {"key": "test-key"},
413
+ "id": "key_01234567890abcdef",
414
+ "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
415
+ "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==",
416
+ }
417
+
418
+ # Mock create_data_key for encryption
419
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
420
+
421
+ plaintext = "Hello, World!"
422
+ context = KeyContext({"key": "test-key"})
423
+ associated_data = "additional-context"
424
+ encrypted_data = self.vault.encrypt(
425
+ data=plaintext, key_context=context, associated_data=associated_data
426
+ )
427
+
428
+ # Now mock decrypt_data_key for decryption
429
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
430
+
431
+ # Decrypt the data with the same associated data
432
+ decrypted_text = self.vault.decrypt(
433
+ encrypted_data=encrypted_data, associated_data=associated_data
434
+ )
435
+
436
+ # Verify decryption worked
437
+ assert decrypted_text == plaintext
438
+
439
+ def test_encrypt_decrypt_roundtrip(
440
+ self, mock_data_key_pair, mock_data_key, capture_and_mock_http_client_request
441
+ ):
442
+ """Test that encrypt/decrypt works correctly together"""
443
+
444
+ # Mock create_data_key for encryption
445
+ capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200)
446
+
447
+ plaintext = "This is a test message for encryption!"
448
+ context = KeyContext({"env": "test", "service": "vault"})
449
+
450
+ # Encrypt the data
451
+ encrypted_data = self.vault.encrypt(data=plaintext, key_context=context)
452
+
453
+ # Mock decrypt_data_key for decryption
454
+ capture_and_mock_http_client_request(self.http_client, mock_data_key, 200)
455
+
456
+ # Decrypt the data
457
+ decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data)
458
+
459
+ # Verify roundtrip worked
460
+ assert decrypted_text == plaintext
@@ -12,7 +12,7 @@ __package_name__ = "workos"
12
12
 
13
13
  __package_url__ = "https://github.com/workos-inc/workos-python"
14
14
 
15
- __version__ = "5.23.0"
15
+ __version__ = "5.26.0"
16
16
 
17
17
  __author__ = "WorkOS"
18
18
 
@@ -11,6 +11,7 @@ from workos.directory_sync import DirectorySyncModule
11
11
  from workos.events import EventsModule
12
12
  from workos.mfa import MFAModule
13
13
  from workos.organizations import OrganizationsModule
14
+ from workos.organization_domains import OrganizationDomainsModule
14
15
  from workos.passwordless import PasswordlessModule
15
16
  from workos.portal import PortalModule
16
17
  from workos.sso import SSOModule
@@ -88,6 +89,10 @@ class BaseClient(ClientConfiguration):
88
89
  @abstractmethod
89
90
  def organizations(self) -> OrganizationsModule: ...
90
91
 
92
+ @property
93
+ @abstractmethod
94
+ def organization_domains(self) -> OrganizationDomainsModule: ...
95
+
91
96
  @property
92
97
  @abstractmethod
93
98
  def passwordless(self) -> PasswordlessModule: ...