From 35ba44e96151efd4fac77546a019e83f96c73de7 Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Fri, 13 Feb 2026 10:52:05 -0400 Subject: [PATCH 1/3] Add authorization organization roles support Add CRUD operations for organization roles including create, list, get, update, set/add/remove permissions on the authorization module. Co-Authored-By: Claude Opus 4.6 --- tests/test_authorization.py | 168 +++++++++++ .../utils/fixtures/mock_organization_role.py | 24 ++ workos/authorization.py | 269 +++++++++++++++++- workos/types/authorization/__init__.py | 4 + .../types/authorization/organization_role.py | 21 ++ 5 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 tests/utils/fixtures/mock_organization_role.py create mode 100644 workos/types/authorization/organization_role.py diff --git a/tests/test_authorization.py b/tests/test_authorization.py index dd00bd53..a4d0b1a8 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -1,6 +1,7 @@ from typing import Union import pytest +from tests.utils.fixtures.mock_organization_role import MockOrganizationRole from tests.utils.fixtures.mock_permission import MockPermission from tests.utils.list_resource import list_response_of from tests.utils.syncify import syncify @@ -170,3 +171,170 @@ def test_delete_permission(self, capture_and_mock_http_client_request): assert request_kwargs["url"].endswith( "/authorization/permissions/documents:read" ) + + # --- Organization Role fixtures --- + + @pytest.fixture + def mock_organization_role(self): + return MockOrganizationRole(id="role_01ABC").dict() + + @pytest.fixture + def mock_organization_roles(self): + return { + "data": [MockOrganizationRole(id=f"role_{i}").dict() for i in range(5)], + "object": "list", + } + + # --- Organization Role tests --- + + def test_create_organization_role( + self, mock_organization_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_role, 201 + ) + + role = syncify( + self.authorization.create_organization_role( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", + slug="admin", + name="Admin", + ) + ) + + assert role.id == "role_01ABC" + assert role.type == "OrganizationRole" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles" + ) + assert request_kwargs["json"] == {"slug": "admin", "name": "Admin"} + + def test_list_organization_roles( + self, mock_organization_roles, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_roles, 200 + ) + + roles_response = syncify( + self.authorization.list_organization_roles( + "org_01EHT88Z8J8795GZNQ4ZP1J81T" + ) + ) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles" + ) + assert len(roles_response.data) == 5 + + def test_get_organization_role( + self, mock_organization_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_role, 200 + ) + + role = syncify( + self.authorization.get_organization_role( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", "admin" + ) + ) + + assert role.id == "role_01ABC" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin" + ) + + def test_update_organization_role( + self, mock_organization_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_role, 200 + ) + + role = syncify( + self.authorization.update_organization_role( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "admin", + name="Super Admin", + ) + ) + + assert role.id == "role_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin" + ) + assert request_kwargs["json"] == {"name": "Super Admin"} + + def test_set_organization_role_permissions( + self, mock_organization_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_role, 200 + ) + + role = syncify( + self.authorization.set_organization_role_permissions( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "admin", + permissions=["documents:read", "documents:write"], + ) + ) + + assert role.id == "role_01ABC" + assert request_kwargs["method"] == "put" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions" + ) + assert request_kwargs["json"] == { + "permissions": ["documents:read", "documents:write"] + } + + def test_add_organization_role_permission( + self, mock_organization_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_organization_role, 200 + ) + + role = syncify( + self.authorization.add_organization_role_permission( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "admin", + permission_slug="documents:read", + ) + ) + + assert role.id == "role_01ABC" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions" + ) + assert request_kwargs["json"] == {"slug": "documents:read"} + + def test_remove_organization_role_permission( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + 202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.remove_organization_role_permission( + "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "admin", + permission_slug="documents:read", + ) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions/documents:read" + ) diff --git a/tests/utils/fixtures/mock_organization_role.py b/tests/utils/fixtures/mock_organization_role.py new file mode 100644 index 00000000..f2b5ac8c --- /dev/null +++ b/tests/utils/fixtures/mock_organization_role.py @@ -0,0 +1,24 @@ +import datetime + +from workos.types.authorization.organization_role import OrganizationRole + + +class MockOrganizationRole(OrganizationRole): + def __init__( + self, + id: str, + organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", + ): + now = datetime.datetime.now().isoformat() + super().__init__( + object="role", + id=id, + organization_id=organization_id, + name="Admin", + slug="admin", + description="Organization admin role", + permissions=["documents:read", "documents:write"], + type="OrganizationRole", + created_at=now, + updated_at=now, + ) diff --git a/workos/authorization.py b/workos/authorization.py index 9fb0985f..2805652b 100644 --- a/workos/authorization.py +++ b/workos/authorization.py @@ -1,5 +1,9 @@ -from typing import Any, Dict, Optional, Protocol +from typing import Any, Dict, Optional, Protocol, Sequence +from workos.types.authorization.organization_role import ( + OrganizationRole, + OrganizationRoleList, +) from workos.types.authorization.permission import Permission from workos.types.list_resource import ( ListArgs, @@ -16,6 +20,7 @@ REQUEST_METHOD_GET, REQUEST_METHOD_PATCH, REQUEST_METHOD_POST, + REQUEST_METHOD_PUT, ) AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" @@ -62,6 +67,58 @@ def update_permission( def delete_permission(self, slug: str) -> SyncOrAsync[None]: ... + # Organization Roles + + def create_organization_role( + self, + organization_id: str, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> SyncOrAsync[OrganizationRole]: ... + + def list_organization_roles( + self, organization_id: str + ) -> SyncOrAsync[OrganizationRoleList]: ... + + def get_organization_role( + self, organization_id: str, slug: str + ) -> SyncOrAsync[OrganizationRole]: ... + + def update_organization_role( + self, + organization_id: str, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> SyncOrAsync[OrganizationRole]: ... + + def set_organization_role_permissions( + self, + organization_id: str, + slug: str, + *, + permissions: Sequence[str], + ) -> SyncOrAsync[OrganizationRole]: ... + + def add_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> SyncOrAsync[OrganizationRole]: ... + + def remove_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> SyncOrAsync[None]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -150,6 +207,110 @@ def delete_permission(self, slug: str) -> None: method=REQUEST_METHOD_DELETE, ) + # Organization Roles + + def create_organization_role( + self, + organization_id: str, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> OrganizationRole: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_POST, + json=json, + ) + + return OrganizationRole.model_validate(response) + + def list_organization_roles(self, organization_id: str) -> OrganizationRoleList: + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_GET, + ) + + return OrganizationRoleList.model_validate(response) + + def get_organization_role( + self, organization_id: str, slug: str + ) -> OrganizationRole: + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}", + method=REQUEST_METHOD_GET, + ) + + return OrganizationRole.model_validate(response) + + def update_organization_role( + self, + organization_id: str, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> OrganizationRole: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return OrganizationRole.model_validate(response) + + def set_organization_role_permissions( + self, + organization_id: str, + slug: str, + *, + permissions: Sequence[str], + ) -> OrganizationRole: + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + method=REQUEST_METHOD_PUT, + json={"permissions": list(permissions)}, + ) + + return OrganizationRole.model_validate(response) + + def add_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> OrganizationRole: + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + method=REQUEST_METHOD_POST, + json={"slug": permission_slug}, + ) + + return OrganizationRole.model_validate(response) + + def remove_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> None: + self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", + method=REQUEST_METHOD_DELETE, + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -237,3 +398,109 @@ async def delete_permission(self, slug: str) -> None: f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", method=REQUEST_METHOD_DELETE, ) + + # Organization Roles + + async def create_organization_role( + self, + organization_id: str, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> OrganizationRole: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_POST, + json=json, + ) + + return OrganizationRole.model_validate(response) + + async def list_organization_roles( + self, organization_id: str + ) -> OrganizationRoleList: + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_GET, + ) + + return OrganizationRoleList.model_validate(response) + + async def get_organization_role( + self, organization_id: str, slug: str + ) -> OrganizationRole: + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}", + method=REQUEST_METHOD_GET, + ) + + return OrganizationRole.model_validate(response) + + async def update_organization_role( + self, + organization_id: str, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> OrganizationRole: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return OrganizationRole.model_validate(response) + + async def set_organization_role_permissions( + self, + organization_id: str, + slug: str, + *, + permissions: Sequence[str], + ) -> OrganizationRole: + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + method=REQUEST_METHOD_PUT, + json={"permissions": list(permissions)}, + ) + + return OrganizationRole.model_validate(response) + + async def add_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> OrganizationRole: + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + method=REQUEST_METHOD_POST, + json={"slug": permission_slug}, + ) + + return OrganizationRole.model_validate(response) + + async def remove_organization_role_permission( + self, + organization_id: str, + slug: str, + *, + permission_slug: str, + ) -> None: + await self._http_client.request( + f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", + method=REQUEST_METHOD_DELETE, + ) diff --git a/workos/types/authorization/__init__.py b/workos/types/authorization/__init__.py index 19893511..93dfa580 100644 --- a/workos/types/authorization/__init__.py +++ b/workos/types/authorization/__init__.py @@ -1,3 +1,7 @@ +from workos.types.authorization.organization_role import ( + OrganizationRole as OrganizationRole, + OrganizationRoleList as OrganizationRoleList, +) from workos.types.authorization.permission import ( Permission as Permission, ) diff --git a/workos/types/authorization/organization_role.py b/workos/types/authorization/organization_role.py new file mode 100644 index 00000000..5214d428 --- /dev/null +++ b/workos/types/authorization/organization_role.py @@ -0,0 +1,21 @@ +from typing import Literal, Optional, Sequence + +from workos.types.workos_model import WorkOSModel + + +class OrganizationRole(WorkOSModel): + object: Literal["role"] + id: str + organization_id: str + name: str + slug: str + description: Optional[str] = None + permissions: Sequence[str] + type: Literal["OrganizationRole"] + created_at: str + updated_at: str + + +class OrganizationRoleList(WorkOSModel): + object: Literal["list"] + data: Sequence[OrganizationRole] From b0e2df2cd194e7f27d3fb4634726992feea753f1 Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Fri, 13 Feb 2026 11:46:10 -0400 Subject: [PATCH 2/3] format --- tests/test_authorization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_authorization.py b/tests/test_authorization.py index a4d0b1a8..cc6feddc 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -218,9 +218,7 @@ def test_list_organization_roles( ) roles_response = syncify( - self.authorization.list_organization_roles( - "org_01EHT88Z8J8795GZNQ4ZP1J81T" - ) + self.authorization.list_organization_roles("org_01EHT88Z8J8795GZNQ4ZP1J81T") ) assert request_kwargs["method"] == "get" From 871b4ea4384b43f2643ae9f3ca1e323dcb90f8f4 Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Tue, 17 Feb 2026 11:23:08 -0400 Subject: [PATCH 3/3] Add authorization environment roles support (#550) * Add authorization environment roles support Add CRUD operations for environment roles including create, list, get, update, set/add permissions on the authorization module. Co-Authored-By: Claude Opus 4.6 * Use Role union type for list/get organization role endpoints The list and get organization role endpoints can return both EnvironmentRole and OrganizationRole types. This aligns the Python SDK return types with the Node SDK. Co-Authored-By: Claude Opus 4.6 * Add authorization event and webhook types (#551) * Add authorization event and webhook types Add event and webhook types for organization_role (created, updated, deleted) and permission (created, updated, deleted) to support authorization-related event streaming and webhook delivery. Co-Authored-By: Claude Opus 4.6 * Distinct type for organization role events * mypy --------- Co-authored-by: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- tests/test_authorization.py | 111 ++++++++ tests/utils/fixtures/mock_environment_role.py | 19 ++ workos/authorization.py | 245 ++++++++++++++++-- workos/types/authorization/__init__.py | 9 + .../types/authorization/environment_role.py | 20 ++ .../types/authorization/organization_role.py | 15 +- workos/types/authorization/role.py | 18 ++ workos/types/events/event.py | 32 +++ workos/types/events/event_model.py | 8 + workos/types/events/event_type.py | 6 + workos/types/webhooks/webhook.py | 32 +++ 11 files changed, 497 insertions(+), 18 deletions(-) create mode 100644 tests/utils/fixtures/mock_environment_role.py create mode 100644 workos/types/authorization/environment_role.py create mode 100644 workos/types/authorization/role.py diff --git a/tests/test_authorization.py b/tests/test_authorization.py index cc6feddc..cd78a3e2 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -1,6 +1,7 @@ from typing import Union import pytest +from tests.utils.fixtures.mock_environment_role import MockEnvironmentRole from tests.utils.fixtures.mock_organization_role import MockOrganizationRole from tests.utils.fixtures.mock_permission import MockPermission from tests.utils.list_resource import list_response_of @@ -336,3 +337,113 @@ def test_remove_organization_role_permission( assert request_kwargs["url"].endswith( "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions/documents:read" ) + + # --- Environment Role fixtures --- + + @pytest.fixture + def mock_environment_role(self): + return MockEnvironmentRole(id="role_01DEF").dict() + + @pytest.fixture + def mock_environment_roles(self): + return { + "data": [MockEnvironmentRole(id=f"role_{i}").dict() for i in range(5)], + "object": "list", + } + + # --- Environment Role tests --- + + def test_create_environment_role( + self, mock_environment_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_role, 201 + ) + + role = syncify( + self.authorization.create_environment_role(slug="member", name="Member") + ) + + assert role.id == "role_01DEF" + assert role.type == "EnvironmentRole" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/roles") + assert request_kwargs["json"] == {"slug": "member", "name": "Member"} + + def test_list_environment_roles( + self, mock_environment_roles, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_roles, 200 + ) + + roles_response = syncify(self.authorization.list_environment_roles()) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/roles") + assert len(roles_response.data) == 5 + + def test_get_environment_role( + self, mock_environment_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_role, 200 + ) + + role = syncify(self.authorization.get_environment_role("member")) + + assert role.id == "role_01DEF" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/roles/member") + + def test_update_environment_role( + self, mock_environment_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_role, 200 + ) + + role = syncify( + self.authorization.update_environment_role("member", name="Updated Member") + ) + + assert role.id == "role_01DEF" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/roles/member") + assert request_kwargs["json"] == {"name": "Updated Member"} + + def test_set_environment_role_permissions( + self, mock_environment_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_role, 200 + ) + + role = syncify( + self.authorization.set_environment_role_permissions( + "member", permissions=["documents:read"] + ) + ) + + assert role.id == "role_01DEF" + assert request_kwargs["method"] == "put" + assert request_kwargs["url"].endswith("/authorization/roles/member/permissions") + assert request_kwargs["json"] == {"permissions": ["documents:read"]} + + def test_add_environment_role_permission( + self, mock_environment_role, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_environment_role, 200 + ) + + role = syncify( + self.authorization.add_environment_role_permission( + "member", permission_slug="documents:read" + ) + ) + + assert role.id == "role_01DEF" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/roles/member/permissions") + assert request_kwargs["json"] == {"slug": "documents:read"} diff --git a/tests/utils/fixtures/mock_environment_role.py b/tests/utils/fixtures/mock_environment_role.py new file mode 100644 index 00000000..6063f3ff --- /dev/null +++ b/tests/utils/fixtures/mock_environment_role.py @@ -0,0 +1,19 @@ +import datetime + +from workos.types.authorization.environment_role import EnvironmentRole + + +class MockEnvironmentRole(EnvironmentRole): + def __init__(self, id: str): + now = datetime.datetime.now().isoformat() + super().__init__( + object="role", + id=id, + name="Member", + slug="member", + description="Default environment member role", + permissions=["documents:read"], + type="EnvironmentRole", + created_at=now, + updated_at=now, + ) diff --git a/workos/authorization.py b/workos/authorization.py index 2805652b..eb4cbfe8 100644 --- a/workos/authorization.py +++ b/workos/authorization.py @@ -1,10 +1,17 @@ -from typing import Any, Dict, Optional, Protocol, Sequence +from typing import Any, Dict, Optional, Protocol, Sequence, Union +from pydantic import TypeAdapter + +from workos.types.authorization.environment_role import ( + EnvironmentRole, + EnvironmentRoleList, +) from workos.types.authorization.organization_role import ( OrganizationRole, OrganizationRoleList, ) from workos.types.authorization.permission import Permission +from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( ListArgs, ListMetadata, @@ -25,6 +32,8 @@ AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" +_role_adapter: TypeAdapter[Role] = TypeAdapter(Role) + class PermissionListFilters(ListArgs, total=False): pass @@ -80,11 +89,11 @@ def create_organization_role( def list_organization_roles( self, organization_id: str - ) -> SyncOrAsync[OrganizationRoleList]: ... + ) -> SyncOrAsync[RoleList]: ... def get_organization_role( self, organization_id: str, slug: str - ) -> SyncOrAsync[OrganizationRole]: ... + ) -> SyncOrAsync[Role]: ... def update_organization_role( self, @@ -119,6 +128,42 @@ def remove_organization_role_permission( permission_slug: str, ) -> SyncOrAsync[None]: ... + # Environment Roles + + def create_environment_role( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> SyncOrAsync[EnvironmentRole]: ... + + def list_environment_roles(self) -> SyncOrAsync[EnvironmentRoleList]: ... + + def get_environment_role(self, slug: str) -> SyncOrAsync[EnvironmentRole]: ... + + def update_environment_role( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> SyncOrAsync[EnvironmentRole]: ... + + def set_environment_role_permissions( + self, + slug: str, + *, + permissions: Sequence[str], + ) -> SyncOrAsync[EnvironmentRole]: ... + + def add_environment_role_permission( + self, + slug: str, + *, + permission_slug: str, + ) -> SyncOrAsync[EnvironmentRole]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -229,23 +274,21 @@ def create_organization_role( return OrganizationRole.model_validate(response) - def list_organization_roles(self, organization_id: str) -> OrganizationRoleList: + def list_organization_roles(self, organization_id: str) -> RoleList: response = self._http_client.request( f"authorization/organizations/{organization_id}/roles", method=REQUEST_METHOD_GET, ) - return OrganizationRoleList.model_validate(response) + return RoleList.model_validate(response) - def get_organization_role( - self, organization_id: str, slug: str - ) -> OrganizationRole: + def get_organization_role(self, organization_id: str, slug: str) -> Role: response = self._http_client.request( f"authorization/organizations/{organization_id}/roles/{slug}", method=REQUEST_METHOD_GET, ) - return OrganizationRole.model_validate(response) + return _role_adapter.validate_python(response) def update_organization_role( self, @@ -311,6 +354,92 @@ def remove_organization_role_permission( method=REQUEST_METHOD_DELETE, ) + # Environment Roles + + def create_environment_role( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> EnvironmentRole: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = self._http_client.request( + "authorization/roles", + method=REQUEST_METHOD_POST, + json=json, + ) + + return EnvironmentRole.model_validate(response) + + def list_environment_roles(self) -> EnvironmentRoleList: + response = self._http_client.request( + "authorization/roles", + method=REQUEST_METHOD_GET, + ) + + return EnvironmentRoleList.model_validate(response) + + def get_environment_role(self, slug: str) -> EnvironmentRole: + response = self._http_client.request( + f"authorization/roles/{slug}", + method=REQUEST_METHOD_GET, + ) + + return EnvironmentRole.model_validate(response) + + def update_environment_role( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> EnvironmentRole: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = self._http_client.request( + f"authorization/roles/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return EnvironmentRole.model_validate(response) + + def set_environment_role_permissions( + self, + slug: str, + *, + permissions: Sequence[str], + ) -> EnvironmentRole: + response = self._http_client.request( + f"authorization/roles/{slug}/permissions", + method=REQUEST_METHOD_PUT, + json={"permissions": list(permissions)}, + ) + + return EnvironmentRole.model_validate(response) + + def add_environment_role_permission( + self, + slug: str, + *, + permission_slug: str, + ) -> EnvironmentRole: + response = self._http_client.request( + f"authorization/roles/{slug}/permissions", + method=REQUEST_METHOD_POST, + json={"slug": permission_slug}, + ) + + return EnvironmentRole.model_validate(response) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -421,25 +550,21 @@ async def create_organization_role( return OrganizationRole.model_validate(response) - async def list_organization_roles( - self, organization_id: str - ) -> OrganizationRoleList: + async def list_organization_roles(self, organization_id: str) -> RoleList: response = await self._http_client.request( f"authorization/organizations/{organization_id}/roles", method=REQUEST_METHOD_GET, ) - return OrganizationRoleList.model_validate(response) + return RoleList.model_validate(response) - async def get_organization_role( - self, organization_id: str, slug: str - ) -> OrganizationRole: + async def get_organization_role(self, organization_id: str, slug: str) -> Role: response = await self._http_client.request( f"authorization/organizations/{organization_id}/roles/{slug}", method=REQUEST_METHOD_GET, ) - return OrganizationRole.model_validate(response) + return _role_adapter.validate_python(response) async def update_organization_role( self, @@ -504,3 +629,89 @@ async def remove_organization_role_permission( f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", method=REQUEST_METHOD_DELETE, ) + + # Environment Roles + + async def create_environment_role( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> EnvironmentRole: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = await self._http_client.request( + "authorization/roles", + method=REQUEST_METHOD_POST, + json=json, + ) + + return EnvironmentRole.model_validate(response) + + async def list_environment_roles(self) -> EnvironmentRoleList: + response = await self._http_client.request( + "authorization/roles", + method=REQUEST_METHOD_GET, + ) + + return EnvironmentRoleList.model_validate(response) + + async def get_environment_role(self, slug: str) -> EnvironmentRole: + response = await self._http_client.request( + f"authorization/roles/{slug}", + method=REQUEST_METHOD_GET, + ) + + return EnvironmentRole.model_validate(response) + + async def update_environment_role( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> EnvironmentRole: + json: Dict[str, Any] = {} + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description + + response = await self._http_client.request( + f"authorization/roles/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return EnvironmentRole.model_validate(response) + + async def set_environment_role_permissions( + self, + slug: str, + *, + permissions: Sequence[str], + ) -> EnvironmentRole: + response = await self._http_client.request( + f"authorization/roles/{slug}/permissions", + method=REQUEST_METHOD_PUT, + json={"permissions": list(permissions)}, + ) + + return EnvironmentRole.model_validate(response) + + async def add_environment_role_permission( + self, + slug: str, + *, + permission_slug: str, + ) -> EnvironmentRole: + response = await self._http_client.request( + f"authorization/roles/{slug}/permissions", + method=REQUEST_METHOD_POST, + json={"slug": permission_slug}, + ) + + return EnvironmentRole.model_validate(response) diff --git a/workos/types/authorization/__init__.py b/workos/types/authorization/__init__.py index 93dfa580..33cde309 100644 --- a/workos/types/authorization/__init__.py +++ b/workos/types/authorization/__init__.py @@ -1,7 +1,16 @@ +from workos.types.authorization.environment_role import ( + EnvironmentRole as EnvironmentRole, + EnvironmentRoleList as EnvironmentRoleList, +) from workos.types.authorization.organization_role import ( OrganizationRole as OrganizationRole, + OrganizationRoleEvent as OrganizationRoleEvent, OrganizationRoleList as OrganizationRoleList, ) from workos.types.authorization.permission import ( Permission as Permission, ) +from workos.types.authorization.role import ( + Role as Role, + RoleList as RoleList, +) diff --git a/workos/types/authorization/environment_role.py b/workos/types/authorization/environment_role.py new file mode 100644 index 00000000..a73d8ed5 --- /dev/null +++ b/workos/types/authorization/environment_role.py @@ -0,0 +1,20 @@ +from typing import Literal, Optional, Sequence + +from workos.types.workos_model import WorkOSModel + + +class EnvironmentRole(WorkOSModel): + object: Literal["role"] + id: str + name: str + slug: str + description: Optional[str] = None + permissions: Sequence[str] + type: Literal["EnvironmentRole"] + created_at: str + updated_at: str + + +class EnvironmentRoleList(WorkOSModel): + object: Literal["list"] + data: Sequence[EnvironmentRole] diff --git a/workos/types/authorization/organization_role.py b/workos/types/authorization/organization_role.py index 5214d428..2e343ad6 100644 --- a/workos/types/authorization/organization_role.py +++ b/workos/types/authorization/organization_role.py @@ -6,7 +6,6 @@ class OrganizationRole(WorkOSModel): object: Literal["role"] id: str - organization_id: str name: str slug: str description: Optional[str] = None @@ -16,6 +15,20 @@ class OrganizationRole(WorkOSModel): updated_at: str +class OrganizationRoleEvent(WorkOSModel): + """Organization role type for Events API responses.""" + + object: Literal["organization_role"] + organization_id: str + slug: str + name: str + description: Optional[str] = None + resource_type_slug: str + permissions: Sequence[str] + created_at: str + updated_at: str + + class OrganizationRoleList(WorkOSModel): object: Literal["list"] data: Sequence[OrganizationRole] diff --git a/workos/types/authorization/role.py b/workos/types/authorization/role.py new file mode 100644 index 00000000..7ea6f747 --- /dev/null +++ b/workos/types/authorization/role.py @@ -0,0 +1,18 @@ +from typing import Literal, Sequence, Union + +from pydantic import Field +from typing_extensions import Annotated + +from workos.types.authorization.environment_role import EnvironmentRole +from workos.types.authorization.organization_role import OrganizationRole +from workos.types.workos_model import WorkOSModel + +Role = Annotated[ + Union[EnvironmentRole, OrganizationRole], + Field(discriminator="type"), +] + + +class RoleList(WorkOSModel): + object: Literal["list"] + data: Sequence[Role] diff --git a/workos/types/events/event.py b/workos/types/events/event.py index 6df63091..fa5bd63f 100644 --- a/workos/types/events/event.py +++ b/workos/types/events/event.py @@ -32,6 +32,8 @@ from workos.types.events.directory_user_with_previous_attributes import ( DirectoryUserWithPreviousAttributes, ) +from workos.types.authorization.organization_role import OrganizationRoleEvent +from workos.types.authorization.permission import Permission from workos.types.events.event_model import EventModel from workos.types.events.organization_domain_verification_failed_payload import ( OrganizationDomainVerificationFailedPayload, @@ -225,6 +227,18 @@ class OrganizationMembershipUpdatedEvent(EventModel[OrganizationMembership]): event: Literal["organization_membership.updated"] +class OrganizationRoleCreatedEvent(EventModel[OrganizationRoleEvent]): + event: Literal["organization_role.created"] + + +class OrganizationRoleUpdatedEvent(EventModel[OrganizationRoleEvent]): + event: Literal["organization_role.updated"] + + +class OrganizationRoleDeletedEvent(EventModel[OrganizationRoleEvent]): + event: Literal["organization_role.deleted"] + + class PasswordResetCreatedEvent(EventModel[PasswordResetCommon]): event: Literal["password_reset.created"] @@ -233,6 +247,18 @@ class PasswordResetSucceededEvent(EventModel[PasswordResetCommon]): event: Literal["password_reset.succeeded"] +class PermissionCreatedEvent(EventModel[Permission]): + event: Literal["permission.created"] + + +class PermissionUpdatedEvent(EventModel[Permission]): + event: Literal["permission.updated"] + + +class PermissionDeletedEvent(EventModel[Permission]): + event: Literal["permission.deleted"] + + class RoleCreatedEvent(EventModel[EventRole]): event: Literal["role.created"] @@ -302,8 +328,14 @@ class UserUpdatedEvent(EventModel[User]): OrganizationMembershipCreatedEvent, OrganizationMembershipDeletedEvent, OrganizationMembershipUpdatedEvent, + OrganizationRoleCreatedEvent, + OrganizationRoleUpdatedEvent, + OrganizationRoleDeletedEvent, PasswordResetCreatedEvent, PasswordResetSucceededEvent, + PermissionCreatedEvent, + PermissionUpdatedEvent, + PermissionDeletedEvent, RoleCreatedEvent, RoleDeletedEvent, RoleUpdatedEvent, diff --git a/workos/types/events/event_model.py b/workos/types/events/event_model.py index 24443a52..dec276ae 100644 --- a/workos/types/events/event_model.py +++ b/workos/types/events/event_model.py @@ -38,6 +38,11 @@ from workos.types.events.session_created_payload import SessionCreatedPayload from workos.types.organizations.organization_common import OrganizationCommon from workos.types.organization_domains import OrganizationDomain +from workos.types.authorization.organization_role import ( + OrganizationRole, + OrganizationRoleEvent, +) +from workos.types.authorization.permission import Permission from workos.types.roles.role import EventRole from workos.types.sso.connection import Connection from workos.types.user_management.email_verification import ( @@ -79,7 +84,10 @@ OrganizationDomain, OrganizationDomainVerificationFailedPayload, OrganizationMembership, + OrganizationRole, + OrganizationRoleEvent, PasswordResetCommon, + Permission, SessionCreatedPayload, User, ) diff --git a/workos/types/events/event_type.py b/workos/types/events/event_type.py index 79856e54..8ba263e3 100644 --- a/workos/types/events/event_type.py +++ b/workos/types/events/event_type.py @@ -44,8 +44,14 @@ "organization_membership.created", "organization_membership.deleted", "organization_membership.updated", + "organization_role.created", + "organization_role.updated", + "organization_role.deleted", "password_reset.created", "password_reset.succeeded", + "permission.created", + "permission.updated", + "permission.deleted", "role.created", "role.deleted", "role.updated", diff --git a/workos/types/webhooks/webhook.py b/workos/types/webhooks/webhook.py index afe9e9ff..3a4c21b3 100644 --- a/workos/types/webhooks/webhook.py +++ b/workos/types/webhooks/webhook.py @@ -37,6 +37,8 @@ OrganizationDomainVerificationFailedPayload, ) from workos.types.events.session_created_payload import SessionCreatedPayload +from workos.types.authorization.organization_role import OrganizationRole +from workos.types.authorization.permission import Permission from workos.types.organization_domains import OrganizationDomain from workos.types.organizations.organization_common import OrganizationCommon from workos.types.roles.role import EventRole @@ -231,6 +233,18 @@ class OrganizationMembershipUpdatedWebhook(WebhookModel[OrganizationMembership]) event: Literal["organization_membership.updated"] +class OrganizationRoleCreatedWebhook(WebhookModel[OrganizationRole]): + event: Literal["organization_role.created"] + + +class OrganizationRoleUpdatedWebhook(WebhookModel[OrganizationRole]): + event: Literal["organization_role.updated"] + + +class OrganizationRoleDeletedWebhook(WebhookModel[OrganizationRole]): + event: Literal["organization_role.deleted"] + + class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]): event: Literal["password_reset.created"] @@ -239,6 +253,18 @@ class PasswordResetSucceededWebhook(WebhookModel[PasswordResetCommon]): event: Literal["password_reset.succeeded"] +class PermissionCreatedWebhook(WebhookModel[Permission]): + event: Literal["permission.created"] + + +class PermissionUpdatedWebhook(WebhookModel[Permission]): + event: Literal["permission.updated"] + + +class PermissionDeletedWebhook(WebhookModel[Permission]): + event: Literal["permission.deleted"] + + class RoleCreatedWebhook(WebhookModel[EventRole]): event: Literal["role.created"] @@ -308,8 +334,14 @@ class UserUpdatedWebhook(WebhookModel[User]): OrganizationMembershipCreatedWebhook, OrganizationMembershipDeletedWebhook, OrganizationMembershipUpdatedWebhook, + OrganizationRoleCreatedWebhook, + OrganizationRoleUpdatedWebhook, + OrganizationRoleDeletedWebhook, PasswordResetCreatedWebhook, PasswordResetSucceededWebhook, + PermissionCreatedWebhook, + PermissionUpdatedWebhook, + PermissionDeletedWebhook, RoleCreatedWebhook, RoleDeletedWebhook, RoleUpdatedWebhook,