Skip to content
511 changes: 510 additions & 1 deletion src/workos/authorization.py

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/workos/types/authorization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
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.resource_identifier import (
ResourceIdentifier,
ResourceIdentifierByExternalId,
ResourceIdentifierById,
)
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
27 changes: 27 additions & 0 deletions src/workos/types/authorization/organization_membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Any, Literal, Mapping, Optional

from workos.types.user_management.organization_membership_status import (
OrganizationMembershipStatus,
)
from workos.types.workos_model import WorkOSModel
from workos.typing.literals import LiteralOrUntyped


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
15 changes: 15 additions & 0 deletions src/workos/types/authorization/resource_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Union

from typing_extensions import TypedDict


class ResourceIdentifierById(TypedDict):
resource_id: str


class ResourceIdentifierByExternalId(TypedDict):
resource_external_id: str
resource_type_slug: str


ResourceIdentifier = Union[ResourceIdentifierById, ResourceIdentifierByExternalId]
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
2 changes: 1 addition & 1 deletion src/workos/types/user_management/list_filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional, Sequence
from workos.types.list_resource import ListArgs
from workos.types.user_management.organization_membership import (
from workos.types.user_management.organization_membership_status import (
OrganizationMembershipStatus,
)

Expand Down
5 changes: 3 additions & 2 deletions src/workos/types/user_management/organization_membership.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import Any, Literal, Mapping, Optional, Sequence
from typing_extensions import TypedDict

from workos.types.user_management.organization_membership_status import (
OrganizationMembershipStatus,
)
from workos.types.workos_model import WorkOSModel
from workos.typing.literals import LiteralOrUntyped

OrganizationMembershipStatus = Literal["active", "inactive", "pending"]


class OrganizationMembershipRole(TypedDict):
slug: str
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing import Literal

OrganizationMembershipStatus = Literal["active", "inactive", "pending"]
11 changes: 7 additions & 4 deletions src/workos/utils/_base_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def _prepare_request(
json: JsonType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
force_include_body: bool = False,
exclude_none: bool = True,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add docstring for this argument?

) -> PreparedRequest:
"""Executes a request against the WorkOS API.
Expand All @@ -133,7 +135,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,19 +152,19 @@ 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
if params is not None:
params = {k: v for k, v in params.items() if v is not None}

# Remove any body values that are None
if json is not None and isinstance(json, Mapping):
if exclude_none and json is not None and isinstance(json, Mapping):
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
50 changes: 49 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 @@ -88,6 +88,7 @@ def request(
json: JsonType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
exclude_none: bool = True,
) -> ResponseJson:
"""Executes a request against the WorkOS API.

Expand All @@ -98,6 +99,7 @@ def request(
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
params (ParamsType): Query params to be added to the request
json (JsonType): Body payload to be added to the request
exclude_none (bool): If True, removes None values from the JSON body

Returns:
ResponseJson: Response from WorkOS
Expand All @@ -109,6 +111,28 @@ def request(
json=json,
headers=headers,
exclude_default_auth_headers=exclude_default_auth_headers,
exclude_none=exclude_none,
)
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)
Expand Down Expand Up @@ -185,6 +209,7 @@ async def request(
json: JsonType = None,
headers: HeadersType = None,
exclude_default_auth_headers: bool = False,
exclude_none: bool = True,
) -> ResponseJson:
"""Executes a request against the WorkOS API.

Expand All @@ -195,6 +220,7 @@ async def request(
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
params (ParamsType): Query params to be added to the request
json (JsonType): Body payload to be added to the request
exclude_none (bool): If True, removes None values from the JSON body

Returns:
ResponseJson: Response from WorkOS
Expand All @@ -206,6 +232,28 @@ async def request(
json=json,
headers=headers,
exclude_default_auth_headers=exclude_default_auth_headers,
exclude_none=exclude_none,
)
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)
Expand Down
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