Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/workos/types/authorization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
from workos.types.authorization.access_evaluation import AccessEvaluation
from workos.types.authorization.environment_role import (
EnvironmentRole,
EnvironmentRoleList,
)
from workos.types.authorization.organization_membership import (
AuthorizationOrganizationMembership,
)
from workos.types.authorization.organization_role import (
OrganizationRole,
OrganizationRoleEvent,
OrganizationRoleList,
)
from workos.types.authorization.permission import Permission
from workos.types.authorization.resource import Resource
from workos.types.authorization.role import (
Role,
RoleList,
)
from workos.types.authorization.role_assignment import (
RoleAssignment,
RoleAssignmentResource,
RoleAssignmentRole,
)
7 changes: 7 additions & 0 deletions src/workos/types/authorization/access_evaluation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from workos.types.workos_model import WorkOSModel


class AccessEvaluation(WorkOSModel):
"""Representation of a WorkOS Authorization access check result."""

authorized: bool
26 changes: 26 additions & 0 deletions src/workos/types/authorization/organization_membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Any, Literal, Mapping, Optional

from workos.types.workos_model import WorkOSModel
from workos.typing.literals import LiteralOrUntyped

OrganizationMembershipStatus = Literal["active", "inactive", "pending"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicated OrganizationMembershipStatus literal

This exact same Literal["active", "inactive", "pending"] type alias is already defined in src/workos/types/user_management/organization_membership.py:7. Consider importing from a shared location or re-exporting from one module to the other to avoid the definitions drifting apart over time.



class AuthorizationOrganizationMembership(WorkOSModel):
"""Representation of an Organization Membership returned by Authorization endpoints.

This is a separate type from the user_management OrganizationMembership because
authorization endpoints return memberships without the ``role`` field and include
``organization_name``. Additionally, ``custom_attributes`` is optional here as
authorization endpoints may omit it.
"""

object: Literal["organization_membership"]
id: str
user_id: str
organization_id: str
organization_name: str
status: LiteralOrUntyped[OrganizationMembershipStatus]
custom_attributes: Optional[Mapping[str, Any]] = None
created_at: str
updated_at: str
18 changes: 18 additions & 0 deletions src/workos/types/authorization/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Literal, Optional

from workos.types.workos_model import WorkOSModel


class Resource(WorkOSModel):
"""Representation of an Authorization Resource."""

object: Literal["authorization_resource"]
id: str
external_id: str
name: str
description: Optional[str] = None
resource_type_slug: str
organization_id: str
parent_resource_id: Optional[str] = None
created_at: str
updated_at: str
22 changes: 22 additions & 0 deletions src/workos/types/authorization/role_assignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Literal

from workos.types.workos_model import WorkOSModel


class RoleAssignmentRole(WorkOSModel):
slug: str


class RoleAssignmentResource(WorkOSModel):
id: str
external_id: str
resource_type_slug: str


class RoleAssignment(WorkOSModel):
object: Literal["role_assignment"]
id: str
role: RoleAssignmentRole
resource: RoleAssignmentResource
created_at: str
updated_at: str
8 changes: 8 additions & 0 deletions src/workos/types/list_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
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.organization_membership import (
AuthorizationOrganizationMembership,
)
from workos.types.authorization.permission import Permission
from workos.types.authorization.resource import Resource
from workos.types.authorization.role_assignment import RoleAssignment
from workos.types.directory_sync import (
Directory,
DirectoryGroup,
Expand Down Expand Up @@ -59,6 +64,9 @@
Organization,
OrganizationMembership,
Permission,
Resource,
RoleAssignment,
AuthorizationOrganizationMembership,
AuthorizationResource,
AuthorizationResourceType,
User,
Expand Down
8 changes: 5 additions & 3 deletions src/workos/utils/_base_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def _prepare_request(
json: JsonType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
force_include_body: bool = False,
) -> PreparedRequest:
"""Executes a request against the WorkOS API.

Expand All @@ -133,7 +134,8 @@ def _prepare_request(
method Optional[str]: One of the supported methods as defined by the REQUEST_METHOD_X constants
params Optional[dict]: Query params or body payload to be added to the request
headers Optional[dict]: Custom headers to be added to the request
token Optional[str]: Bearer token
exclude_default_auth_headers (bool): If True, excludes default auth headers from the request
force_include_body (bool): If True, allows sending a body in a bodyless request (used for DELETE requests)

Returns:
dict: Response from WorkOS
Expand All @@ -149,7 +151,7 @@ def _prepare_request(
REQUEST_METHOD_GET,
]

if bodyless_http_method and json is not None:
if bodyless_http_method and json is not None and not force_include_body:
raise ValueError(f"Cannot send a body with a {parsed_method} request")

# Remove any parameters that are None
Expand All @@ -161,7 +163,7 @@ def _prepare_request(
json = {k: v for k, v in json.items() if v is not None}

# We'll spread these return values onto the HTTP client request method
if bodyless_http_method:
if bodyless_http_method and not force_include_body:
return {
"method": parsed_method,
"url": url,
Expand Down
44 changes: 43 additions & 1 deletion src/workos/utils/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
ParamsType,
ResponseJson,
)
from workos.utils.request_helper import REQUEST_METHOD_GET
from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_GET


class SyncHttpxClientWrapper(httpx.Client):
Expand Down Expand Up @@ -113,6 +113,27 @@ def request(
response = self._client.request(**prepared_request_parameters)
return self._handle_response(response)

def delete_with_body(
self,
path: str,
json: JsonType = None,
params: ParamsType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
) -> ResponseJson:
"""Executes a DELETE request with a JSON body against the WorkOS API."""
prepared_request_parameters = self._prepare_request(
path=path,
method=REQUEST_METHOD_DELETE,
json=json,
params=params,
headers=headers,
exclude_default_auth_headers=exclude_default_auth_headers,
force_include_body=True,
)
response = self._client.request(**prepared_request_parameters)
return self._handle_response(response)


class AsyncHttpxClientWrapper(httpx.AsyncClient):
def __del__(self) -> None:
Expand Down Expand Up @@ -210,5 +231,26 @@ async def request(
response = await self._client.request(**prepared_request_parameters)
return self._handle_response(response)

async def delete_with_body(
self,
path: str,
json: JsonType = None,
params: ParamsType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
) -> ResponseJson:
"""Executes a DELETE request with a JSON body against the WorkOS API."""
prepared_request_parameters = self._prepare_request(
path=path,
method=REQUEST_METHOD_DELETE,
json=json,
params=params,
headers=headers,
exclude_default_auth_headers=exclude_default_auth_headers,
force_include_body=True,
)
response = await self._client.request(**prepared_request_parameters)
return self._handle_response(response)


HTTPClient = Union[AsyncHTTPClient, SyncHTTPClient]
37 changes: 37 additions & 0 deletions tests/test_async_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,40 @@ async def test_request_removes_none_json_values(
json={"organization_id": None, "test": "value"},
)
assert request_kwargs["json"] == {"test": "value"}

async def test_delete_with_body_sends_json(
self, capture_and_mock_http_client_request
):
request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200)

await self.http_client.delete_with_body(
path="/test",
json={"resource_id": "res_01ABC"},
)

assert request_kwargs["method"] == "delete"
assert request_kwargs["json"] == {"resource_id": "res_01ABC"}

async def test_delete_with_body_sends_params(
self, capture_and_mock_http_client_request
):
request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200)

await self.http_client.delete_with_body(
path="/test",
json={"resource_id": "res_01ABC"},
params={"org_id": "org_01ABC"},
)

assert request_kwargs["params"] == {"org_id": "org_01ABC"}
assert request_kwargs["json"] == {"resource_id": "res_01ABC"}

async def test_delete_without_body_raises_value_error(self):
with pytest.raises(
ValueError, match="Cannot send a body with a delete request"
):
await self.http_client.request(
path="/test",
method="delete",
json={"should": "fail"},
)
Loading