Skip to content

Commit e623448

Browse files
FGA implementation pr1
1 parent 526eb7d commit e623448

File tree

3 files changed

+388
-2
lines changed

3 files changed

+388
-2
lines changed

src/workos/authorization.py

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
from typing import Any, Dict, Optional, Protocol, Sequence
1+
from typing import Any, Dict, Optional, Protocol, Sequence, Union
22

33
from pydantic import TypeAdapter
4+
from typing_extensions import TypedDict
45

56
from workos.types.authorization.environment_role import (
67
EnvironmentRole,
78
EnvironmentRoleList,
89
)
910
from workos.types.authorization.organization_role import OrganizationRole
1011
from workos.types.authorization.permission import Permission
12+
from workos.types.authorization.resource import Resource
1113
from workos.types.authorization.role import Role, RoleList
1214
from workos.types.list_resource import (
1315
ListArgs,
@@ -28,6 +30,24 @@
2830
)
2931

3032
AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions"
33+
AUTHORIZATION_RESOURCES_PATH = "authorization/resources"
34+
35+
36+
class ParentResourceById(TypedDict):
37+
"""Identify a parent resource by its WorkOS resource ID."""
38+
39+
resource_id: str
40+
41+
42+
class ParentResourceByExternalId(TypedDict):
43+
"""Identify a parent resource by organization, type, and external ID."""
44+
45+
organization_id: str
46+
resource_type: str
47+
external_id: str
48+
49+
50+
ParentResource = Union[ParentResourceById, ParentResourceByExternalId]
3151

3252
_role_adapter: TypeAdapter[Role] = TypeAdapter(Role)
3353

@@ -161,6 +181,34 @@ def add_environment_role_permission(
161181
permission_slug: str,
162182
) -> SyncOrAsync[EnvironmentRole]: ...
163183

184+
# Resources
185+
186+
def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ...
187+
188+
def create_resource(
189+
self,
190+
*,
191+
resource_type: str,
192+
organization_id: str,
193+
external_id: Optional[str] = None,
194+
meta: Optional[Dict[str, Any]] = None,
195+
parent: Optional[ParentResource] = None,
196+
) -> SyncOrAsync[Resource]: ...
197+
198+
def update_resource(
199+
self,
200+
resource_id: str,
201+
*,
202+
meta: Optional[Dict[str, Any]] = None,
203+
) -> SyncOrAsync[Resource]: ...
204+
205+
def delete_resource(
206+
self,
207+
resource_id: str,
208+
*,
209+
cascade_delete: Optional[bool] = None,
210+
) -> SyncOrAsync[None]: ...
211+
164212

165213
class Authorization(AuthorizationModule):
166214
_http_client: SyncHTTPClient
@@ -437,6 +485,79 @@ def add_environment_role_permission(
437485

438486
return EnvironmentRole.model_validate(response)
439487

488+
# Resources
489+
490+
def get_resource(self, resource_id: str) -> Resource:
491+
response = self._http_client.request(
492+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
493+
method=REQUEST_METHOD_GET,
494+
)
495+
496+
return Resource.model_validate(response)
497+
498+
def create_resource(
499+
self,
500+
*,
501+
resource_type: str,
502+
organization_id: str,
503+
external_id: Optional[str] = None,
504+
meta: Optional[Dict[str, Any]] = None,
505+
parent: Optional[ParentResource] = None,
506+
) -> Resource:
507+
json: Dict[str, Any] = {
508+
"resource_type": resource_type,
509+
"organization_id": organization_id,
510+
}
511+
if external_id is not None:
512+
json["external_id"] = external_id
513+
if meta is not None:
514+
json["meta"] = meta
515+
if parent is not None:
516+
json["parent"] = parent
517+
518+
response = self._http_client.request(
519+
AUTHORIZATION_RESOURCES_PATH,
520+
method=REQUEST_METHOD_POST,
521+
json=json,
522+
)
523+
524+
return Resource.model_validate(response)
525+
526+
def update_resource(
527+
self,
528+
resource_id: str,
529+
*,
530+
meta: Optional[Dict[str, Any]] = None,
531+
) -> Resource:
532+
json: Dict[str, Any] = {}
533+
if meta is not None:
534+
json["meta"] = meta
535+
536+
response = self._http_client.request(
537+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
538+
method=REQUEST_METHOD_PATCH,
539+
json=json,
540+
)
541+
542+
return Resource.model_validate(response)
543+
544+
def delete_resource(
545+
self,
546+
resource_id: str,
547+
*,
548+
cascade_delete: Optional[bool] = None,
549+
) -> None:
550+
if cascade_delete is not None:
551+
self._http_client.delete_with_body(
552+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
553+
json={"cascade_delete": cascade_delete},
554+
)
555+
else:
556+
self._http_client.request(
557+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
558+
method=REQUEST_METHOD_DELETE,
559+
)
560+
440561

441562
class AsyncAuthorization(AuthorizationModule):
442563
_http_client: AsyncHTTPClient
@@ -712,3 +833,76 @@ async def add_environment_role_permission(
712833
)
713834

714835
return EnvironmentRole.model_validate(response)
836+
837+
# Resources
838+
839+
async def get_resource(self, resource_id: str) -> Resource:
840+
response = await self._http_client.request(
841+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
842+
method=REQUEST_METHOD_GET,
843+
)
844+
845+
return Resource.model_validate(response)
846+
847+
async def create_resource(
848+
self,
849+
*,
850+
resource_type: str,
851+
organization_id: str,
852+
external_id: Optional[str] = None,
853+
meta: Optional[Dict[str, Any]] = None,
854+
parent: Optional[ParentResource] = None,
855+
) -> Resource:
856+
json: Dict[str, Any] = {
857+
"resource_type": resource_type,
858+
"organization_id": organization_id,
859+
}
860+
if external_id is not None:
861+
json["external_id"] = external_id
862+
if meta is not None:
863+
json["meta"] = meta
864+
if parent is not None:
865+
json["parent"] = parent
866+
867+
response = await self._http_client.request(
868+
AUTHORIZATION_RESOURCES_PATH,
869+
method=REQUEST_METHOD_POST,
870+
json=json,
871+
)
872+
873+
return Resource.model_validate(response)
874+
875+
async def update_resource(
876+
self,
877+
resource_id: str,
878+
*,
879+
meta: Optional[Dict[str, Any]] = None,
880+
) -> Resource:
881+
json: Dict[str, Any] = {}
882+
if meta is not None:
883+
json["meta"] = meta
884+
885+
response = await self._http_client.request(
886+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
887+
method=REQUEST_METHOD_PATCH,
888+
json=json,
889+
)
890+
891+
return Resource.model_validate(response)
892+
893+
async def delete_resource(
894+
self,
895+
resource_id: str,
896+
*,
897+
cascade_delete: Optional[bool] = None,
898+
) -> None:
899+
if cascade_delete is not None:
900+
await self._http_client.delete_with_body(
901+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
902+
json={"cascade_delete": cascade_delete},
903+
)
904+
else:
905+
await self._http_client.request(
906+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
907+
method=REQUEST_METHOD_DELETE,
908+
)

src/workos/types/authorization/resource.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Optional
1+
from typing import Any, Literal, Mapping, Optional
22

33
from workos.types.workos_model import WorkOSModel
44

@@ -14,5 +14,8 @@ class Resource(WorkOSModel):
1414
resource_type_slug: str
1515
organization_id: str
1616
parent_resource_id: Optional[str] = None
17+
# The API returns meta when set via create_resource / update_resource.
18+
# Without this field the model would silently discard that data.
19+
meta: Optional[Mapping[str, Any]] = None
1720
created_at: str
1821
updated_at: str

0 commit comments

Comments
 (0)