From 87f4b680a7015647d272b3e51a3597055fad01dc Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Fri, 13 Feb 2026 10:49:54 -0400 Subject: [PATCH 1/4] Add authorization permissions support Introduce the authorization module with CRUD operations for permissions including create, list (paginated), get, update (PATCH), and delete. Register the module on both sync and async clients. Co-Authored-By: Claude Opus 4.6 --- src/workos/_base_client.py | 5 + src/workos/async_client.py | 7 + src/workos/authorization.py | 239 +++++++++++++++++++ src/workos/client.py | 7 + src/workos/types/authorization/__init__.py | 3 + src/workos/types/authorization/permission.py | 14 ++ src/workos/types/list_resource.py | 2 + src/workos/utils/request_helper.py | 1 + tests/test_authorization.py | 172 +++++++++++++ tests/utils/fixtures/mock_permission.py | 18 ++ 10 files changed, 468 insertions(+) create mode 100644 src/workos/authorization.py create mode 100644 src/workos/types/authorization/__init__.py create mode 100644 src/workos/types/authorization/permission.py create mode 100644 tests/test_authorization.py create mode 100644 tests/utils/fixtures/mock_permission.py diff --git a/src/workos/_base_client.py b/src/workos/_base_client.py index 4f8f50e2..24d2f5e8 100644 --- a/src/workos/_base_client.py +++ b/src/workos/_base_client.py @@ -5,6 +5,7 @@ from workos._client_configuration import ClientConfiguration from workos.api_keys import ApiKeysModule from workos.audit_logs import AuditLogsModule +from workos.authorization import AuthorizationModule from workos.directory_sync import DirectorySyncModule from workos.events import EventsModule from workos.fga import FGAModule @@ -74,6 +75,10 @@ def __init__( @abstractmethod def api_keys(self) -> ApiKeysModule: ... + @property + @abstractmethod + def authorization(self) -> AuthorizationModule: ... + @property @abstractmethod def audit_logs(self) -> AuditLogsModule: ... diff --git a/src/workos/async_client.py b/src/workos/async_client.py index 11cec994..c050865f 100644 --- a/src/workos/async_client.py +++ b/src/workos/async_client.py @@ -3,6 +3,7 @@ from workos._base_client import BaseClient from workos.api_keys import AsyncApiKeys from workos.audit_logs import AsyncAuditLogs +from workos.authorization import AsyncAuthorization from workos.directory_sync import AsyncDirectorySync from workos.events import AsyncEvents from workos.fga import FGAModule @@ -55,6 +56,12 @@ def api_keys(self) -> AsyncApiKeys: self._api_keys = AsyncApiKeys(self._http_client) return self._api_keys + @property + def authorization(self) -> AsyncAuthorization: + if not getattr(self, "_authorization", None): + self._authorization = AsyncAuthorization(self._http_client) + return self._authorization + @property def sso(self) -> AsyncSSO: if not getattr(self, "_sso", None): diff --git a/src/workos/authorization.py b/src/workos/authorization.py new file mode 100644 index 00000000..9fb0985f --- /dev/null +++ b/src/workos/authorization.py @@ -0,0 +1,239 @@ +from typing import Any, Dict, Optional, Protocol + +from workos.types.authorization.permission import Permission +from workos.types.list_resource import ( + ListArgs, + ListMetadata, + ListPage, + WorkOSListResource, +) +from workos.typing.sync_or_async import SyncOrAsync +from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient +from workos.utils.pagination_order import PaginationOrder +from workos.utils.request_helper import ( + DEFAULT_LIST_RESPONSE_LIMIT, + REQUEST_METHOD_DELETE, + REQUEST_METHOD_GET, + REQUEST_METHOD_PATCH, + REQUEST_METHOD_POST, +) + +AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" + + +class PermissionListFilters(ListArgs, total=False): + pass + + +PermissionsListResource = WorkOSListResource[ + Permission, PermissionListFilters, ListMetadata +] + + +class AuthorizationModule(Protocol): + """Offers methods through the WorkOS Authorization service.""" + + def create_permission( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> SyncOrAsync[Permission]: ... + + def list_permissions( + self, + *, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> SyncOrAsync[PermissionsListResource]: ... + + def get_permission(self, slug: str) -> SyncOrAsync[Permission]: ... + + def update_permission( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> SyncOrAsync[Permission]: ... + + def delete_permission(self, slug: str) -> SyncOrAsync[None]: ... + + +class Authorization(AuthorizationModule): + _http_client: SyncHTTPClient + + def __init__(self, http_client: SyncHTTPClient): + self._http_client = http_client + + def create_permission( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> Permission: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = self._http_client.request( + AUTHORIZATION_PERMISSIONS_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Permission.model_validate(response) + + def list_permissions( + self, + *, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> PermissionsListResource: + list_params: PermissionListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + response = self._http_client.request( + AUTHORIZATION_PERMISSIONS_PATH, + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[Permission, PermissionListFilters, ListMetadata]( + list_method=self.list_permissions, + list_args=list_params, + **ListPage[Permission](**response).model_dump(), + ) + + def get_permission(self, slug: str) -> Permission: + response = self._http_client.request( + f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_GET, + ) + + return Permission.model_validate(response) + + def update_permission( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> Permission: + 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_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Permission.model_validate(response) + + def delete_permission(self, slug: str) -> None: + self._http_client.request( + f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_DELETE, + ) + + +class AsyncAuthorization(AuthorizationModule): + _http_client: AsyncHTTPClient + + def __init__(self, http_client: AsyncHTTPClient): + self._http_client = http_client + + async def create_permission( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + ) -> Permission: + json: Dict[str, Any] = {"slug": slug, "name": name} + if description is not None: + json["description"] = description + + response = await self._http_client.request( + AUTHORIZATION_PERMISSIONS_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Permission.model_validate(response) + + async def list_permissions( + self, + *, + limit: int = DEFAULT_LIST_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + order: PaginationOrder = "desc", + ) -> PermissionsListResource: + list_params: PermissionListFilters = { + "limit": limit, + "before": before, + "after": after, + "order": order, + } + + response = await self._http_client.request( + AUTHORIZATION_PERMISSIONS_PATH, + method=REQUEST_METHOD_GET, + params=list_params, + ) + + return WorkOSListResource[Permission, PermissionListFilters, ListMetadata]( + list_method=self.list_permissions, + list_args=list_params, + **ListPage[Permission](**response).model_dump(), + ) + + async def get_permission(self, slug: str) -> Permission: + response = await self._http_client.request( + f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_GET, + ) + + return Permission.model_validate(response) + + async def update_permission( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + ) -> Permission: + 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_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Permission.model_validate(response) + + async def delete_permission(self, slug: str) -> None: + await self._http_client.request( + f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", + method=REQUEST_METHOD_DELETE, + ) diff --git a/src/workos/client.py b/src/workos/client.py index d7da65d6..68b9a287 100644 --- a/src/workos/client.py +++ b/src/workos/client.py @@ -3,6 +3,7 @@ from workos._base_client import BaseClient from workos.api_keys import ApiKeys from workos.audit_logs import AuditLogs +from workos.authorization import Authorization from workos.directory_sync import DirectorySync from workos.fga import FGA from workos.organizations import Organizations @@ -55,6 +56,12 @@ def api_keys(self) -> ApiKeys: self._api_keys = ApiKeys(self._http_client) return self._api_keys + @property + def authorization(self) -> Authorization: + if not getattr(self, "_authorization", None): + self._authorization = Authorization(self._http_client) + return self._authorization + @property def sso(self) -> SSO: if not getattr(self, "_sso", None): diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py new file mode 100644 index 00000000..19893511 --- /dev/null +++ b/src/workos/types/authorization/__init__.py @@ -0,0 +1,3 @@ +from workos.types.authorization.permission import ( + Permission as Permission, +) diff --git a/src/workos/types/authorization/permission.py b/src/workos/types/authorization/permission.py new file mode 100644 index 00000000..13f9d7a8 --- /dev/null +++ b/src/workos/types/authorization/permission.py @@ -0,0 +1,14 @@ +from typing import Literal, Optional + +from workos.types.workos_model import WorkOSModel + + +class Permission(WorkOSModel): + object: Literal["permission"] + id: str + slug: str + name: str + description: Optional[str] = None + system: bool + created_at: str + updated_at: str diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py index 14cbb600..e8621ab9 100644 --- a/src/workos/types/list_resource.py +++ b/src/workos/types/list_resource.py @@ -19,6 +19,7 @@ from typing_extensions import Required, TypedDict from workos.types.api_keys import ApiKey from workos.types.audit_logs import AuditLogAction, AuditLogSchema +from workos.types.authorization.permission import Permission from workos.types.directory_sync import ( Directory, DirectoryGroup, @@ -57,6 +58,7 @@ Invitation, Organization, OrganizationMembership, + Permission, AuthorizationResource, AuthorizationResourceType, User, diff --git a/src/workos/utils/request_helper.py b/src/workos/utils/request_helper.py index 933b3376..8b240255 100644 --- a/src/workos/utils/request_helper.py +++ b/src/workos/utils/request_helper.py @@ -6,6 +6,7 @@ RESPONSE_TYPE_CODE = "code" REQUEST_METHOD_DELETE = "delete" REQUEST_METHOD_GET = "get" +REQUEST_METHOD_PATCH = "patch" REQUEST_METHOD_POST = "post" REQUEST_METHOD_PUT = "put" diff --git a/tests/test_authorization.py b/tests/test_authorization.py new file mode 100644 index 00000000..dd00bd53 --- /dev/null +++ b/tests/test_authorization.py @@ -0,0 +1,172 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_permission import MockPermission +from tests.utils.list_resource import list_response_of +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorization: + @pytest.fixture(autouse=True) + def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): + self.http_client = module_instance._http_client + self.authorization = module_instance + + @pytest.fixture + def mock_permission(self): + return MockPermission(id="perm_01ABC").dict() + + @pytest.fixture + def mock_permissions(self): + permission_list = [ + MockPermission(id=f"perm_{i}", slug=f"perm-{i}").dict() for i in range(5) + ] + return { + "data": permission_list, + "list_metadata": {"before": None, "after": None}, + "object": "list", + } + + @pytest.fixture + def mock_permissions_multiple_data_pages(self): + permission_list = [ + MockPermission(id=f"perm_{i}", slug=f"perm-{i}").dict() for i in range(40) + ] + return list_response_of(data=permission_list) + + def test_create_permission( + self, mock_permission, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permission, 201 + ) + + permission = syncify( + self.authorization.create_permission( + slug="documents:read", name="Read Documents" + ) + ) + + assert permission.id == "perm_01ABC" + assert permission.slug == "documents:read" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/permissions") + assert request_kwargs["json"] == { + "slug": "documents:read", + "name": "Read Documents", + } + + def test_create_permission_with_description( + self, mock_permission, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permission, 201 + ) + + syncify( + self.authorization.create_permission( + slug="documents:read", + name="Read Documents", + description="Allows reading documents", + ) + ) + + assert request_kwargs["json"] == { + "slug": "documents:read", + "name": "Read Documents", + "description": "Allows reading documents", + } + + def test_list_permissions( + self, mock_permissions, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permissions, 200 + ) + + permissions_response = syncify(self.authorization.list_permissions()) + + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/permissions") + assert len(permissions_response.data) == 5 + + def test_list_permissions_auto_pagination( + self, + mock_permissions_multiple_data_pages, + test_auto_pagination, + ): + test_auto_pagination( + http_client=self.http_client, + list_function=self.authorization.list_permissions, + expected_all_page_data=mock_permissions_multiple_data_pages["data"], + ) + + def test_get_permission( + self, mock_permission, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permission, 200 + ) + + permission = syncify(self.authorization.get_permission("documents:read")) + + assert permission.id == "perm_01ABC" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith( + "/authorization/permissions/documents:read" + ) + + def test_update_permission( + self, mock_permission, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permission, 200 + ) + + permission = syncify( + self.authorization.update_permission("documents:read", name="Updated Name") + ) + + assert permission.id == "perm_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith( + "/authorization/permissions/documents:read" + ) + assert request_kwargs["json"] == {"name": "Updated Name"} + + def test_update_permission_with_description( + self, mock_permission, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_permission, 200 + ) + + syncify( + self.authorization.update_permission( + "documents:read", + name="Updated Name", + description="Updated description", + ) + ) + + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } + + def test_delete_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.delete_permission("documents:read")) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith( + "/authorization/permissions/documents:read" + ) diff --git a/tests/utils/fixtures/mock_permission.py b/tests/utils/fixtures/mock_permission.py new file mode 100644 index 00000000..b1e639bf --- /dev/null +++ b/tests/utils/fixtures/mock_permission.py @@ -0,0 +1,18 @@ +import datetime + +from workos.types.authorization.permission import Permission + + +class MockPermission(Permission): + def __init__(self, id: str, slug: str = "documents:read"): + now = datetime.datetime.now().isoformat() + super().__init__( + object="permission", + id=id, + slug=slug, + name="Read Documents", + description="Allows reading documents", + system=False, + created_at=now, + updated_at=now, + ) From 9ac71a0f34bff8628c9ba2e60550649a0a602839 Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Tue, 17 Feb 2026 11:25:06 -0400 Subject: [PATCH 2/4] Add authorization organization roles support (#549) * 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 * format * 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 --------- Co-authored-by: Claude Opus 4.6 --- src/workos/authorization.py | 480 +++++++++++++++++- src/workos/types/authorization/__init__.py | 13 + .../types/authorization/environment_role.py | 20 + .../types/authorization/organization_role.py | 34 ++ src/workos/types/authorization/role.py | 18 + src/workos/types/events/event.py | 28 +- src/workos/types/events/event_model.py | 9 +- src/workos/types/events/event_type.py | 4 +- src/workos/types/webhooks/webhook.py | 30 +- tests/test_authorization.py | 277 ++++++++++ tests/utils/fixtures/mock_environment_role.py | 19 + .../utils/fixtures/mock_organization_role.py | 24 + 12 files changed, 925 insertions(+), 31 deletions(-) create mode 100644 src/workos/types/authorization/environment_role.py create mode 100644 src/workos/types/authorization/organization_role.py create mode 100644 src/workos/types/authorization/role.py create mode 100644 tests/utils/fixtures/mock_environment_role.py create mode 100644 tests/utils/fixtures/mock_organization_role.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 9fb0985f..eb4cbfe8 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,6 +1,17 @@ -from typing import Any, Dict, Optional, Protocol +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, @@ -16,10 +27,13 @@ REQUEST_METHOD_GET, REQUEST_METHOD_PATCH, REQUEST_METHOD_POST, + REQUEST_METHOD_PUT, ) AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" +_role_adapter: TypeAdapter[Role] = TypeAdapter(Role) + class PermissionListFilters(ListArgs, total=False): pass @@ -62,6 +76,94 @@ 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[RoleList]: ... + + def get_organization_role( + self, organization_id: str, slug: str + ) -> SyncOrAsync[Role]: ... + + 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]: ... + + # 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 @@ -150,6 +252,194 @@ 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) -> RoleList: + response = self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_GET, + ) + + return RoleList.model_validate(response) + + 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 _role_adapter.validate_python(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, + ) + + # 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 @@ -237,3 +527,191 @@ 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) -> RoleList: + response = await self._http_client.request( + f"authorization/organizations/{organization_id}/roles", + method=REQUEST_METHOD_GET, + ) + + return RoleList.model_validate(response) + + 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 _role_adapter.validate_python(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, + ) + + # 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/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 19893511..33cde309 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -1,3 +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/src/workos/types/authorization/environment_role.py b/src/workos/types/authorization/environment_role.py new file mode 100644 index 00000000..a73d8ed5 --- /dev/null +++ b/src/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/src/workos/types/authorization/organization_role.py b/src/workos/types/authorization/organization_role.py new file mode 100644 index 00000000..2e343ad6 --- /dev/null +++ b/src/workos/types/authorization/organization_role.py @@ -0,0 +1,34 @@ +from typing import Literal, Optional, Sequence + +from workos.types.workos_model import WorkOSModel + + +class OrganizationRole(WorkOSModel): + object: Literal["role"] + id: str + name: str + slug: str + description: Optional[str] = None + permissions: Sequence[str] + type: Literal["OrganizationRole"] + created_at: str + 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/src/workos/types/authorization/role.py b/src/workos/types/authorization/role.py new file mode 100644 index 00000000..7ea6f747 --- /dev/null +++ b/src/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/src/workos/types/events/event.py b/src/workos/types/events/event.py index 582be909..9217c061 100644 --- a/src/workos/types/events/event.py +++ b/src/workos/types/events/event.py @@ -42,6 +42,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.flag_payload import FlagPayload, FlagRuleUpdatedContext from workos.types.events.organization_domain_verification_failed_payload import ( @@ -308,16 +310,17 @@ class OrganizationMembershipUpdatedEvent(EventModel[OrganizationMembership]): event: Literal["organization_membership.updated"] -class OrganizationRoleCreatedEvent(EventModel[OrganizationRolePayload]): +class OrganizationRoleCreatedEvent(EventModel[OrganizationRoleEvent]): event: Literal["organization_role.created"] -class OrganizationRoleDeletedEvent(EventModel[OrganizationRolePayload]): - event: Literal["organization_role.deleted"] +class OrganizationRoleUpdatedEvent(EventModel[OrganizationRoleEvent]): + event: Literal["organization_role.updated"] -class OrganizationRoleUpdatedEvent(EventModel[OrganizationRolePayload]): - event: Literal["organization_role.updated"] +class OrganizationRoleDeletedEvent(EventModel[OrganizationRoleEvent]): + event: Literal["organization_role.deleted"] + class PasswordResetCreatedEvent(EventModel[PasswordResetCommon]): @@ -328,16 +331,17 @@ class PasswordResetSucceededEvent(EventModel[PasswordResetCommon]): event: Literal["password_reset.succeeded"] -class PermissionCreatedEvent(EventModel[PermissionPayload]): +class PermissionCreatedEvent(EventModel[Permission]): event: Literal["permission.created"] -class PermissionDeletedEvent(EventModel[PermissionPayload]): - event: Literal["permission.deleted"] +class PermissionUpdatedEvent(EventModel[Permission]): + event: Literal["permission.updated"] -class PermissionUpdatedEvent(EventModel[PermissionPayload]): - event: Literal["permission.updated"] +class PermissionDeletedEvent(EventModel[Permission]): + event: Literal["permission.deleted"] + class RoleCreatedEvent(EventModel[EventRole]): @@ -428,13 +432,13 @@ class UserUpdatedEvent(EventModel[User]): OrganizationMembershipDeletedEvent, OrganizationMembershipUpdatedEvent, OrganizationRoleCreatedEvent, - OrganizationRoleDeletedEvent, OrganizationRoleUpdatedEvent, + OrganizationRoleDeletedEvent, PasswordResetCreatedEvent, PasswordResetSucceededEvent, PermissionCreatedEvent, - PermissionDeletedEvent, PermissionUpdatedEvent, + PermissionDeletedEvent, RoleCreatedEvent, RoleDeletedEvent, RoleUpdatedEvent, diff --git a/src/workos/types/events/event_model.py b/src/workos/types/events/event_model.py index d010e004..22cc7f22 100644 --- a/src/workos/types/events/event_model.py +++ b/src/workos/types/events/event_model.py @@ -55,6 +55,11 @@ ) 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 ( @@ -105,9 +110,9 @@ OrganizationDomain, OrganizationDomainVerificationFailedPayload, OrganizationMembership, - OrganizationRolePayload, + OrganizationRoleEvent, PasswordResetCommon, - PermissionPayload, + Permission, SessionCreatedPayload, SessionRevokedPayload, User, diff --git a/src/workos/types/events/event_type.py b/src/workos/types/events/event_type.py index ca6f9447..6ccc5d57 100644 --- a/src/workos/types/events/event_type.py +++ b/src/workos/types/events/event_type.py @@ -59,13 +59,13 @@ "organization_membership.deleted", "organization_membership.updated", "organization_role.created", - "organization_role.deleted", "organization_role.updated", + "organization_role.deleted", "password_reset.created", "password_reset.succeeded", "permission.created", - "permission.deleted", "permission.updated", + "permission.deleted", "role.created", "role.deleted", "role.updated", diff --git a/src/workos/types/webhooks/webhook.py b/src/workos/types/webhooks/webhook.py index fa2a47b0..067ce2fd 100644 --- a/src/workos/types/webhooks/webhook.py +++ b/src/workos/types/webhooks/webhook.py @@ -47,12 +47,12 @@ from workos.types.events.organization_domain_verification_failed_payload import ( OrganizationDomainVerificationFailedPayload, ) -from workos.types.events.organization_role_payload import OrganizationRolePayload -from workos.types.events.permission_payload import PermissionPayload from workos.types.events.session_payload import ( SessionCreatedPayload, SessionRevokedPayload, ) +from workos.types.authorization.organization_role import OrganizationRoleEvent +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 @@ -316,16 +316,17 @@ class OrganizationMembershipUpdatedWebhook(WebhookModel[OrganizationMembership]) event: Literal["organization_membership.updated"] -class OrganizationRoleCreatedWebhook(WebhookModel[OrganizationRolePayload]): +class OrganizationRoleCreatedWebhook(WebhookModel[OrganizationRoleEvent]): event: Literal["organization_role.created"] -class OrganizationRoleDeletedWebhook(WebhookModel[OrganizationRolePayload]): - event: Literal["organization_role.deleted"] +class OrganizationRoleUpdatedWebhook(WebhookModel[OrganizationRoleEvent]): + event: Literal["organization_role.updated"] -class OrganizationRoleUpdatedWebhook(WebhookModel[OrganizationRolePayload]): - event: Literal["organization_role.updated"] +class OrganizationRoleDeletedWebhook(WebhookModel[OrganizationRoleEvent]): + event: Literal["organization_role.deleted"] + class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]): @@ -336,16 +337,17 @@ class PasswordResetSucceededWebhook(WebhookModel[PasswordResetCommon]): event: Literal["password_reset.succeeded"] -class PermissionCreatedWebhook(WebhookModel[PermissionPayload]): +class PermissionCreatedWebhook(WebhookModel[Permission]): event: Literal["permission.created"] -class PermissionDeletedWebhook(WebhookModel[PermissionPayload]): - event: Literal["permission.deleted"] +class PermissionUpdatedWebhook(WebhookModel[Permission]): + event: Literal["permission.updated"] -class PermissionUpdatedWebhook(WebhookModel[PermissionPayload]): - event: Literal["permission.updated"] +class PermissionDeletedWebhook(WebhookModel[Permission]): + event: Literal["permission.deleted"] + class RoleCreatedWebhook(WebhookModel[EventRole]): @@ -436,13 +438,13 @@ class UserUpdatedWebhook(WebhookModel[User]): OrganizationMembershipDeletedWebhook, OrganizationMembershipUpdatedWebhook, OrganizationRoleCreatedWebhook, - OrganizationRoleDeletedWebhook, OrganizationRoleUpdatedWebhook, + OrganizationRoleDeletedWebhook, PasswordResetCreatedWebhook, PasswordResetSucceededWebhook, PermissionCreatedWebhook, - PermissionDeletedWebhook, PermissionUpdatedWebhook, + PermissionDeletedWebhook, RoleCreatedWebhook, RoleDeletedWebhook, RoleUpdatedWebhook, diff --git a/tests/test_authorization.py b/tests/test_authorization.py index dd00bd53..cd78a3e2 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -1,6 +1,8 @@ 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 from tests.utils.syncify import syncify @@ -170,3 +172,278 @@ 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" + ) + + # --- 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/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, + ) From d8c773cc85fc50ba6e5142dee9aea23561ea9e5c Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Tue, 17 Feb 2026 11:42:10 -0400 Subject: [PATCH 3/4] Remove aliasing, format --- src/workos/types/authorization/__init__.py | 18 ++++++++---------- src/workos/types/events/event.py | 2 -- src/workos/types/webhooks/webhook.py | 2 -- tests/test_session.py | 6 +++--- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py index 33cde309..609a4f2d 100644 --- a/src/workos/types/authorization/__init__.py +++ b/src/workos/types/authorization/__init__.py @@ -1,16 +1,14 @@ from workos.types.authorization.environment_role import ( - EnvironmentRole as EnvironmentRole, - EnvironmentRoleList as EnvironmentRoleList, + EnvironmentRole, + 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, + OrganizationRole, + OrganizationRoleEvent, + OrganizationRoleList, ) +from workos.types.authorization.permission import Permission from workos.types.authorization.role import ( - Role as Role, - RoleList as RoleList, + Role, + RoleList, ) diff --git a/src/workos/types/events/event.py b/src/workos/types/events/event.py index 9217c061..fe111d58 100644 --- a/src/workos/types/events/event.py +++ b/src/workos/types/events/event.py @@ -322,7 +322,6 @@ class OrganizationRoleDeletedEvent(EventModel[OrganizationRoleEvent]): event: Literal["organization_role.deleted"] - class PasswordResetCreatedEvent(EventModel[PasswordResetCommon]): event: Literal["password_reset.created"] @@ -343,7 +342,6 @@ class PermissionDeletedEvent(EventModel[Permission]): event: Literal["permission.deleted"] - class RoleCreatedEvent(EventModel[EventRole]): event: Literal["role.created"] diff --git a/src/workos/types/webhooks/webhook.py b/src/workos/types/webhooks/webhook.py index 067ce2fd..a7b42823 100644 --- a/src/workos/types/webhooks/webhook.py +++ b/src/workos/types/webhooks/webhook.py @@ -328,7 +328,6 @@ class OrganizationRoleDeletedWebhook(WebhookModel[OrganizationRoleEvent]): event: Literal["organization_role.deleted"] - class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]): event: Literal["password_reset.created"] @@ -349,7 +348,6 @@ class PermissionDeletedWebhook(WebhookModel[Permission]): event: Literal["permission.deleted"] - class RoleCreatedWebhook(WebhookModel[EventRole]): event: Literal["role.created"] diff --git a/tests/test_session.py b/tests/test_session.py index a51b38ba..d995e0f7 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -785,6 +785,6 @@ def get_client(): first_client = clients[0] for client in clients[1:]: - assert client is first_client, ( - "All concurrent calls should return the same instance" - ) + assert ( + client is first_client + ), "All concurrent calls should return the same instance" From f500abb94d17408dac7a10b44af27f04f802b4cc Mon Sep 17 00:00:00 2001 From: Colin Barber Date: Tue, 17 Feb 2026 11:46:01 -0400 Subject: [PATCH 4/4] Format, types --- src/workos/authorization.py | 7 ++----- src/workos/types/events/__init__.py | 2 -- src/workos/types/events/event.py | 2 -- src/workos/types/events/event_model.py | 8 +------- .../types/events/organization_role_payload.py | 14 -------------- src/workos/types/events/permission_payload.py | 13 ------------- tests/test_session.py | 6 +++--- 7 files changed, 6 insertions(+), 46 deletions(-) delete mode 100644 src/workos/types/events/organization_role_payload.py delete mode 100644 src/workos/types/events/permission_payload.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index eb4cbfe8..6e12f035 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Protocol, Sequence, Union +from typing import Any, Dict, Optional, Protocol, Sequence from pydantic import TypeAdapter @@ -6,10 +6,7 @@ EnvironmentRole, EnvironmentRoleList, ) -from workos.types.authorization.organization_role import ( - OrganizationRole, - OrganizationRoleList, -) +from workos.types.authorization.organization_role import OrganizationRole from workos.types.authorization.permission import Permission from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py index 25a293c1..004d6077 100644 --- a/src/workos/types/events/__init__.py +++ b/src/workos/types/events/__init__.py @@ -11,7 +11,5 @@ from .event import * from .flag_payload import * from .organization_domain_verification_failed_payload import * -from .organization_role_payload import * -from .permission_payload import * from .previous_attributes import * from .session_payload import * diff --git a/src/workos/types/events/event.py b/src/workos/types/events/event.py index fe111d58..028e08f3 100644 --- a/src/workos/types/events/event.py +++ b/src/workos/types/events/event.py @@ -49,8 +49,6 @@ from workos.types.events.organization_domain_verification_failed_payload import ( OrganizationDomainVerificationFailedPayload, ) -from workos.types.events.organization_role_payload import OrganizationRolePayload -from workos.types.events.permission_payload import PermissionPayload from workos.types.events.session_payload import ( SessionCreatedPayload, SessionRevokedPayload, diff --git a/src/workos/types/events/event_model.py b/src/workos/types/events/event_model.py index 22cc7f22..2d4fb4dc 100644 --- a/src/workos/types/events/event_model.py +++ b/src/workos/types/events/event_model.py @@ -46,19 +46,13 @@ from workos.types.events.organization_domain_verification_failed_payload import ( OrganizationDomainVerificationFailedPayload, ) -from workos.types.events.organization_role_payload import OrganizationRolePayload -from workos.types.events.permission_payload import PermissionPayload - from workos.types.events.session_payload import ( SessionCreatedPayload, SessionRevokedPayload, ) 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.organization_role import OrganizationRoleEvent from workos.types.authorization.permission import Permission from workos.types.roles.role import EventRole from workos.types.sso.connection import Connection diff --git a/src/workos/types/events/organization_role_payload.py b/src/workos/types/events/organization_role_payload.py deleted file mode 100644 index e257baff..00000000 --- a/src/workos/types/events/organization_role_payload.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Literal, Optional, Sequence -from workos.types.workos_model import WorkOSModel - - -class OrganizationRolePayload(WorkOSModel): - 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 diff --git a/src/workos/types/events/permission_payload.py b/src/workos/types/events/permission_payload.py deleted file mode 100644 index 514af392..00000000 --- a/src/workos/types/events/permission_payload.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel - - -class PermissionPayload(WorkOSModel): - object: Literal["permission"] - id: str - slug: str - name: str - description: Optional[str] = None - system: bool - created_at: str - updated_at: str diff --git a/tests/test_session.py b/tests/test_session.py index d995e0f7..a51b38ba 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -785,6 +785,6 @@ def get_client(): first_client = clients[0] for client in clients[1:]: - assert ( - client is first_client - ), "All concurrent calls should return the same instance" + assert client is first_client, ( + "All concurrent calls should return the same instance" + )