Skip to content
Open
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
515 changes: 514 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_check_response import AccessCheckResponse
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,
)
5 changes: 5 additions & 0 deletions src/workos/types/authorization/access_check_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from workos.types.workos_model import WorkOSModel


class AccessCheckResponse(WorkOSModel):
authorized: bool
5 changes: 5 additions & 0 deletions src/workos/types/authorization/organization_membership.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from workos.types.user_management.organization_membership import (
BaseOrganizationMembership,
)

AuthorizationOrganizationMembership = BaseOrganizationMembership
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
25 changes: 14 additions & 11 deletions src/workos/types/user_management/organization_membership.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
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 BaseOrganizationMembership(WorkOSModel):
object: Literal["organization_membership"]
id: str
user_id: str
organization_id: str
status: LiteralOrUntyped[OrganizationMembershipStatus]
custom_attributes: Optional[Mapping[str, Any]] = None
created_at: str
updated_at: str


class OrganizationMembershipRole(TypedDict):
slug: str


class OrganizationMembership(WorkOSModel):
"""Representation of an WorkOS Organization Membership."""

object: Literal["organization_membership"]
id: str
user_id: str
organization_id: str
class OrganizationMembership(BaseOrganizationMembership):
role: OrganizationMembershipRole
roles: Optional[Sequence[OrganizationMembershipRole]] = None
status: LiteralOrUntyped[OrganizationMembershipStatus]
custom_attributes: Mapping[str, Any]
created_at: str
updated_at: str
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from typing import Literal

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.

It's risky to duplicate this if it already exists in another package. Could drift from the other types.

Choose a reason for hiding this comment

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

I actually broke it out out organization_membership.py into it's own file so it could be imported as the single source of truth

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