Skip to content

Commit fb85aa7

Browse files
author
Asaf Cohen
authored
Merge pull request #53 from permitio/asaf/cto-228-add-bulk-relationship-tuple-api-to-python-sdk
add bulk relationship tuple api to python SDK + add missing APIs to sync python client
2 parents 06d22d9 + 2a5782e commit fb85aa7

6 files changed

Lines changed: 250 additions & 2 deletions

File tree

permit/api/api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def role_assignments(self) -> RoleAssignmentsApi:
144144
@property
145145
def relationship_tuples(self) -> RelationshipTuplesApi:
146146
"""
147-
API for managing role assignments.
147+
API for managing relationship tuples.
148148
See: https://api.permit.io/v2/redoc#tag/Relationship-tuples
149149
"""
150150
return self._relationship_tuples

permit/api/models.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,25 @@ class Config:
15781578
)
15791579

15801580

1581+
class RelationshipTupleBlockRead(BaseModel):
1582+
class Config:
1583+
extra = Extra.allow
1584+
1585+
subject: str = Field(
1586+
...,
1587+
description="resource_key:resource_instance_key of the subject",
1588+
title="Subject",
1589+
)
1590+
relation: str = Field(
1591+
..., description="key of the assigned relation", title="Relation"
1592+
)
1593+
object: str = Field(
1594+
...,
1595+
description="resource_key:resource_instance_key of the object",
1596+
title="Object",
1597+
)
1598+
1599+
15811600
class RelationshipTupleCreate(BaseModel):
15821601
class Config:
15831602
extra = Extra.allow
@@ -1604,6 +1623,22 @@ class Config:
16041623
)
16051624

16061625

1626+
class RelationshipTupleCreateBulkOperation(BaseModel):
1627+
class Config:
1628+
extra = Extra.allow
1629+
1630+
operations: List[RelationshipTupleCreate] = Field(
1631+
..., max_items=1000, title="Operations"
1632+
)
1633+
1634+
1635+
class RelationshipTupleCreateBulkOperationResult(BaseModel):
1636+
pass
1637+
1638+
class Config:
1639+
extra = Extra.allow
1640+
1641+
16071642
class RelationshipTupleDelete(BaseModel):
16081643
class Config:
16091644
extra = Extra.allow
@@ -1625,6 +1660,20 @@ class Config:
16251660
)
16261661

16271662

1663+
class RelationshipTupleDeleteBulkOperation(BaseModel):
1664+
class Config:
1665+
extra = Extra.allow
1666+
1667+
idents: List[RelationshipTupleDelete] = Field(..., max_items=1000, title="Idents")
1668+
1669+
1670+
class RelationshipTupleDeleteBulkOperationResult(BaseModel):
1671+
pass
1672+
1673+
class Config:
1674+
extra = Extra.allow
1675+
1676+
16281677
class RelationshipTupleObj(BaseModel):
16291678
class Config:
16301679
extra = Extra.allow

permit/api/relationship_tuples.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
from .context import ApiContextLevel, ApiKeyAccessLevel
1818
from .models import (
1919
RelationshipTupleCreate,
20+
RelationshipTupleCreateBulkOperation,
21+
RelationshipTupleCreateBulkOperationResult,
2022
RelationshipTupleDelete,
23+
RelationshipTupleDeleteBulkOperation,
24+
RelationshipTupleDeleteBulkOperationResult,
2125
RelationshipTupleRead,
2226
)
2327

@@ -100,3 +104,69 @@ async def delete(self, tuple_data: RelationshipTupleDelete) -> None:
100104
PermitContextError: If the configured ApiContext does not match the required endpoint context.
101105
"""
102106
return await self.__relationship_tuples.delete("", json=tuple_data)
107+
108+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
109+
@required_context(ApiContextLevel.ENVIRONMENT)
110+
@validate_arguments
111+
async def bulk_create(
112+
self, tuples: List[RelationshipTupleCreate]
113+
) -> RelationshipTupleCreateBulkOperationResult:
114+
"""
115+
Creates multiple relationship tuples at once using the provided tuple data.
116+
117+
Args:
118+
tuples: The relationship tuples to create.
119+
Each tuple object is of type RelationshipTupleCreate and is essentially
120+
a tuple of (subject, relation, object, tenant).
121+
122+
subject and object are both resource instances, formatted as
123+
`<resourcetype:instancekey>` strings (e.g: Folder:budget23).
124+
relation is the name of the relation.
125+
tenant is the key of the tenant in which to place the relation
126+
(optional if at least one of subject/object already exists).
127+
128+
Subject and object must both be resource instances *in the same tenant*!
129+
130+
Returns:
131+
the tuples creation result (RelationshipTupleCreateBulkOperationResult)
132+
133+
Raises:
134+
PermitApiError: If the API returns an error HTTP status code.
135+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
136+
"""
137+
return await self.__relationship_tuples.post(
138+
"/bulk",
139+
model=RelationshipTupleCreateBulkOperationResult,
140+
json=RelationshipTupleCreateBulkOperation(operations=tuples),
141+
)
142+
143+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
144+
@required_context(ApiContextLevel.ENVIRONMENT)
145+
@validate_arguments
146+
async def bulk_delete(
147+
self, tuples: List[RelationshipTupleDelete]
148+
) -> RelationshipTupleDeleteBulkOperationResult:
149+
"""
150+
Deletes multiple relationship tuples at once using the provided tuple data.
151+
152+
Args:
153+
tuples: The relationship tuples to delete.
154+
Each tuple object is of type RelationshipTupleDelete and is essentially
155+
a tuple of (subject, relation, object).
156+
157+
subject and object are both resource instances, formatted as
158+
`<resourcetype:instancekey>` strings (e.g: Folder:budget23).
159+
relation is the name of the relation.
160+
161+
Returns:
162+
the tuples deletion result (RelationshipTupleDeleteBulkOperationResult)
163+
164+
Raises:
165+
PermitApiError: If the API returns an error HTTP status code.
166+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
167+
"""
168+
return await self.__relationship_tuples.delete(
169+
"/bulk",
170+
model=RelationshipTupleDeleteBulkOperationResult,
171+
json=RelationshipTupleDeleteBulkOperation(idents=tuples),
172+
)

permit/api/sync_api_client.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
from .deprecated import DeprecatedApi
66
from .environments import EnvironmentsApi
77
from .projects import ProjectsApi
8+
from .relationship_tuples import RelationshipTuplesApi
89
from .resource_action_groups import ResourceActionGroupsApi
910
from .resource_actions import ResourceActionsApi
1011
from .resource_attributes import ResourceAttributesApi
12+
from .resource_instances import ResourceInstancesApi
13+
from .resource_relations import ResourceRelationsApi
14+
from .resource_roles import ResourceRolesApi
1115
from .resources import ResourcesApi
1216
from .role_assignments import RoleAssignmentsApi
1317
from .roles import RolesApi
@@ -35,6 +39,10 @@ class SyncProjectsApi(ProjectsApi, metaclass=SyncClass):
3539
pass
3640

3741

42+
class SyncRelationshipTuplesApi(RelationshipTuplesApi, metaclass=SyncClass):
43+
pass
44+
45+
3846
class SyncResourceActionGroupsApi(ResourceActionGroupsApi, metaclass=SyncClass):
3947
pass
4048

@@ -47,6 +55,18 @@ class SyncResourceAttributesApi(ResourceAttributesApi, metaclass=SyncClass):
4755
pass
4856

4957

58+
class SyncResourceInstancesApi(ResourceInstancesApi, metaclass=SyncClass):
59+
pass
60+
61+
62+
class SyncResourceRelationsApi(ResourceRelationsApi, metaclass=SyncClass):
63+
pass
64+
65+
66+
class SyncResourceRolesApi(ResourceRolesApi, metaclass=SyncClass):
67+
pass
68+
69+
5070
class SyncResourcesApi(ResourcesApi, metaclass=SyncClass):
5171
pass
5272

@@ -81,9 +101,13 @@ def __init__(self, config: PermitConfig):
81101
self._condition_sets = SyncConditionSetsApi(config)
82102
self._environments = SyncEnvironmentsApi(config)
83103
self._projects = SyncProjectsApi(config)
104+
self._relationship_tuples = SyncRelationshipTuplesApi(config)
84105
self._action_groups = SyncResourceActionGroupsApi(config)
85106
self._resource_actions = SyncResourceActionsApi(config)
86107
self._resource_attributes = SyncResourceAttributesApi(config)
108+
self._resource_instances = SyncResourceInstancesApi(config)
109+
self._resource_relations = SyncResourceRelationsApi(config)
110+
self._resource_roles = SyncResourceRolesApi(config)
87111
self._resources = SyncResourcesApi(config)
88112
self._role_assignments = SyncRoleAssignmentsApi(config)
89113
self._roles = SyncRolesApi(config)
@@ -146,6 +170,30 @@ def resource_attributes(self) -> SyncResourceAttributesApi:
146170
"""
147171
return self._resource_attributes
148172

173+
@property
174+
def resource_roles(self) -> SyncResourceRolesApi:
175+
"""
176+
API for managing resource roles.
177+
See: https://api.permit.io/v2/redoc#tag/Resource-Roles
178+
"""
179+
return self._resource_roles
180+
181+
@property
182+
def resource_relations(self) -> SyncResourceRelationsApi:
183+
"""
184+
API for managing resource relations.
185+
See: https://api.permit.io/v2/redoc#tag/Resource-Relations
186+
"""
187+
return self._resource_relations
188+
189+
@property
190+
def resource_instances(self) -> SyncResourceInstancesApi:
191+
"""
192+
API for managing resource instances.
193+
See: https://api.permit.io/v2/redoc#tag/Resource-Instances
194+
"""
195+
return self._resource_instances
196+
149197
@property
150198
def resources(self) -> SyncResourcesApi:
151199
"""
@@ -162,6 +210,14 @@ def role_assignments(self) -> SyncRoleAssignmentsApi:
162210
"""
163211
return self._role_assignments
164212

213+
@property
214+
def relationship_tuples(self) -> SyncRelationshipTuplesApi:
215+
"""
216+
API for managing relationship tuples.
217+
See: https://api.permit.io/v2/redoc#tag/Relationship-tuples
218+
"""
219+
return self._relationship_tuples
220+
165221
@property
166222
def roles(self) -> SyncRolesApi:
167223
"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def get_requirements(env=""):
1010

1111
setup(
1212
name="permit",
13-
version="2.2.0",
13+
version="2.2.1",
1414
packages=find_packages(),
1515
author="Asaf Cohen",
1616
author_email="asaf@permit.io",

tests/test_rebac_e2e.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
RelationshipTupleCreate,
1515
RelationshipTupleDelete,
1616
ResourceCreate,
17+
ResourceInstanceCreate,
1718
ResourceRoleCreate,
1819
RoleAssignmentCreate,
1920
RoleAssignmentRemove,
@@ -284,6 +285,18 @@ class PermissionAssertions:
284285
(f"{ACCOUNT.key}:cocacola", "account", f"{FOLDER.key}:recipes", TENANT_CC.key),
285286
]
286287

288+
BULK_RELATIONSHIPS = [
289+
# rnd folder contains 2 documents
290+
(f"{FOLDER.key}:media", "parent", f"{DOCUMENT.key}:movie1", TENANT_PERMIT.key),
291+
(f"{FOLDER.key}:media", "parent", f"{DOCUMENT.key}:movie2", TENANT_PERMIT.key),
292+
]
293+
294+
BULK_RELATIONSHIPS_INSTANCES = [
295+
f"{FOLDER.key}:media",
296+
f"{DOCUMENT.key}:movie1",
297+
f"{DOCUMENT.key}:movie2",
298+
]
299+
287300
ASSIGNMENTS_AND_ASSERTIONS: List[PermissionAssertions] = [
288301
# direct access
289302
PermissionAssertions(
@@ -718,6 +731,66 @@ async def test_rebac_policy(permit: Permit):
718731
assert rel_tuple.relation == relation
719732
assert rel_tuple.object == object
720733
assert rel_tuple.tenant == tenant
734+
735+
tuples = await permit.api.relationship_tuples.list()
736+
len_tuples = len(tuples)
737+
logger.debug(
738+
f"there are currently {len_tuples} relationship tuples in the system"
739+
)
740+
741+
# bulk create relationship tuples
742+
bulk_relationships_to_create = [
743+
RelationshipTupleCreate(
744+
subject=subject, relation=relation, object=object, tenant=tenant
745+
)
746+
for (subject, relation, object, tenant) in BULK_RELATIONSHIPS
747+
]
748+
bulk_relationships_to_delete = [
749+
RelationshipTupleDelete(subject=subject, relation=relation, object=object)
750+
for (subject, relation, object, tenant) in BULK_RELATIONSHIPS
751+
]
752+
753+
for instance_key in BULK_RELATIONSHIPS_INSTANCES:
754+
parts = instance_key.split(":")
755+
logger.debug(f"creating resource instance: {instance_key}")
756+
await permit.api.resource_instances.create(
757+
ResourceInstanceCreate(
758+
key=parts[1], resource=parts[0], tenant=TENANT_PERMIT.key
759+
)
760+
)
761+
762+
async def create_relationships_in_bulk():
763+
await permit.api.relationship_tuples.bulk_create(
764+
tuples=bulk_relationships_to_create
765+
)
766+
767+
tuples = await permit.api.relationship_tuples.list()
768+
assert len(tuples) == len_tuples + len(BULK_RELATIONSHIPS)
769+
logger.debug(
770+
f"there are currently {len(tuples)} relationship tuples in the system"
771+
)
772+
773+
async def remove_relationships_in_bulk():
774+
await permit.api.relationship_tuples.bulk_delete(
775+
tuples=bulk_relationships_to_delete
776+
)
777+
778+
tuples = await permit.api.relationship_tuples.list()
779+
assert len(tuples) == len_tuples
780+
logger.debug(
781+
f"there are currently {len(tuples)} relationship tuples in the system"
782+
)
783+
784+
logger.debug(
785+
f"creating {len(BULK_RELATIONSHIPS)} relationship tuples in bulk: {str(BULK_RELATIONSHIPS)}"
786+
)
787+
await create_relationships_in_bulk()
788+
789+
logger.debug(
790+
f"removing the same {len(BULK_RELATIONSHIPS)} relationship tuples in bulk: {str(BULK_RELATIONSHIPS)}"
791+
)
792+
await remove_relationships_in_bulk()
793+
721794
# assign roles and then run permission checks
722795
for test_step in ASSIGNMENTS_AND_ASSERTIONS:
723796
try:

0 commit comments

Comments
 (0)