Skip to content

Commit

Permalink
Merge pull request #53 from permitio/asaf/cto-228-add-bulk-relationsh…
Browse files Browse the repository at this point in the history
…ip-tuple-api-to-python-sdk

add bulk relationship tuple api to python SDK + add missing APIs to sync python client
  • Loading branch information
asafc authored Jan 10, 2024
2 parents 06d22d9 + 2a5782e commit fb85aa7
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 2 deletions.
2 changes: 1 addition & 1 deletion permit/api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def role_assignments(self) -> RoleAssignmentsApi:
@property
def relationship_tuples(self) -> RelationshipTuplesApi:
"""
API for managing role assignments.
API for managing relationship tuples.
See: https://api.permit.io/v2/redoc#tag/Relationship-tuples
"""
return self._relationship_tuples
Expand Down
49 changes: 49 additions & 0 deletions permit/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,25 @@ class Config:
)


class RelationshipTupleBlockRead(BaseModel):
class Config:
extra = Extra.allow

subject: str = Field(
...,
description="resource_key:resource_instance_key of the subject",
title="Subject",
)
relation: str = Field(
..., description="key of the assigned relation", title="Relation"
)
object: str = Field(
...,
description="resource_key:resource_instance_key of the object",
title="Object",
)


class RelationshipTupleCreate(BaseModel):
class Config:
extra = Extra.allow
Expand All @@ -1604,6 +1623,22 @@ class Config:
)


class RelationshipTupleCreateBulkOperation(BaseModel):
class Config:
extra = Extra.allow

operations: List[RelationshipTupleCreate] = Field(
..., max_items=1000, title="Operations"
)


class RelationshipTupleCreateBulkOperationResult(BaseModel):
pass

class Config:
extra = Extra.allow


class RelationshipTupleDelete(BaseModel):
class Config:
extra = Extra.allow
Expand All @@ -1625,6 +1660,20 @@ class Config:
)


class RelationshipTupleDeleteBulkOperation(BaseModel):
class Config:
extra = Extra.allow

idents: List[RelationshipTupleDelete] = Field(..., max_items=1000, title="Idents")


class RelationshipTupleDeleteBulkOperationResult(BaseModel):
pass

class Config:
extra = Extra.allow


class RelationshipTupleObj(BaseModel):
class Config:
extra = Extra.allow
Expand Down
70 changes: 70 additions & 0 deletions permit/api/relationship_tuples.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
from .context import ApiContextLevel, ApiKeyAccessLevel
from .models import (
RelationshipTupleCreate,
RelationshipTupleCreateBulkOperation,
RelationshipTupleCreateBulkOperationResult,
RelationshipTupleDelete,
RelationshipTupleDeleteBulkOperation,
RelationshipTupleDeleteBulkOperationResult,
RelationshipTupleRead,
)

Expand Down Expand Up @@ -100,3 +104,69 @@ async def delete(self, tuple_data: RelationshipTupleDelete) -> None:
PermitContextError: If the configured ApiContext does not match the required endpoint context.
"""
return await self.__relationship_tuples.delete("", json=tuple_data)

@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
@required_context(ApiContextLevel.ENVIRONMENT)
@validate_arguments
async def bulk_create(
self, tuples: List[RelationshipTupleCreate]
) -> RelationshipTupleCreateBulkOperationResult:
"""
Creates multiple relationship tuples at once using the provided tuple data.
Args:
tuples: The relationship tuples to create.
Each tuple object is of type RelationshipTupleCreate and is essentially
a tuple of (subject, relation, object, tenant).
subject and object are both resource instances, formatted as
`<resourcetype:instancekey>` strings (e.g: Folder:budget23).
relation is the name of the relation.
tenant is the key of the tenant in which to place the relation
(optional if at least one of subject/object already exists).
Subject and object must both be resource instances *in the same tenant*!
Returns:
the tuples creation result (RelationshipTupleCreateBulkOperationResult)
Raises:
PermitApiError: If the API returns an error HTTP status code.
PermitContextError: If the configured ApiContext does not match the required endpoint context.
"""
return await self.__relationship_tuples.post(
"/bulk",
model=RelationshipTupleCreateBulkOperationResult,
json=RelationshipTupleCreateBulkOperation(operations=tuples),
)

@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
@required_context(ApiContextLevel.ENVIRONMENT)
@validate_arguments
async def bulk_delete(
self, tuples: List[RelationshipTupleDelete]
) -> RelationshipTupleDeleteBulkOperationResult:
"""
Deletes multiple relationship tuples at once using the provided tuple data.
Args:
tuples: The relationship tuples to delete.
Each tuple object is of type RelationshipTupleDelete and is essentially
a tuple of (subject, relation, object).
subject and object are both resource instances, formatted as
`<resourcetype:instancekey>` strings (e.g: Folder:budget23).
relation is the name of the relation.
Returns:
the tuples deletion result (RelationshipTupleDeleteBulkOperationResult)
Raises:
PermitApiError: If the API returns an error HTTP status code.
PermitContextError: If the configured ApiContext does not match the required endpoint context.
"""
return await self.__relationship_tuples.delete(
"/bulk",
model=RelationshipTupleDeleteBulkOperationResult,
json=RelationshipTupleDeleteBulkOperation(idents=tuples),
)
56 changes: 56 additions & 0 deletions permit/api/sync_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
from .deprecated import DeprecatedApi
from .environments import EnvironmentsApi
from .projects import ProjectsApi
from .relationship_tuples import RelationshipTuplesApi
from .resource_action_groups import ResourceActionGroupsApi
from .resource_actions import ResourceActionsApi
from .resource_attributes import ResourceAttributesApi
from .resource_instances import ResourceInstancesApi
from .resource_relations import ResourceRelationsApi
from .resource_roles import ResourceRolesApi
from .resources import ResourcesApi
from .role_assignments import RoleAssignmentsApi
from .roles import RolesApi
Expand Down Expand Up @@ -35,6 +39,10 @@ class SyncProjectsApi(ProjectsApi, metaclass=SyncClass):
pass


class SyncRelationshipTuplesApi(RelationshipTuplesApi, metaclass=SyncClass):
pass


class SyncResourceActionGroupsApi(ResourceActionGroupsApi, metaclass=SyncClass):
pass

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


class SyncResourceInstancesApi(ResourceInstancesApi, metaclass=SyncClass):
pass


class SyncResourceRelationsApi(ResourceRelationsApi, metaclass=SyncClass):
pass


class SyncResourceRolesApi(ResourceRolesApi, metaclass=SyncClass):
pass


class SyncResourcesApi(ResourcesApi, metaclass=SyncClass):
pass

Expand Down Expand Up @@ -81,9 +101,13 @@ def __init__(self, config: PermitConfig):
self._condition_sets = SyncConditionSetsApi(config)
self._environments = SyncEnvironmentsApi(config)
self._projects = SyncProjectsApi(config)
self._relationship_tuples = SyncRelationshipTuplesApi(config)
self._action_groups = SyncResourceActionGroupsApi(config)
self._resource_actions = SyncResourceActionsApi(config)
self._resource_attributes = SyncResourceAttributesApi(config)
self._resource_instances = SyncResourceInstancesApi(config)
self._resource_relations = SyncResourceRelationsApi(config)
self._resource_roles = SyncResourceRolesApi(config)
self._resources = SyncResourcesApi(config)
self._role_assignments = SyncRoleAssignmentsApi(config)
self._roles = SyncRolesApi(config)
Expand Down Expand Up @@ -146,6 +170,30 @@ def resource_attributes(self) -> SyncResourceAttributesApi:
"""
return self._resource_attributes

@property
def resource_roles(self) -> SyncResourceRolesApi:
"""
API for managing resource roles.
See: https://api.permit.io/v2/redoc#tag/Resource-Roles
"""
return self._resource_roles

@property
def resource_relations(self) -> SyncResourceRelationsApi:
"""
API for managing resource relations.
See: https://api.permit.io/v2/redoc#tag/Resource-Relations
"""
return self._resource_relations

@property
def resource_instances(self) -> SyncResourceInstancesApi:
"""
API for managing resource instances.
See: https://api.permit.io/v2/redoc#tag/Resource-Instances
"""
return self._resource_instances

@property
def resources(self) -> SyncResourcesApi:
"""
Expand All @@ -162,6 +210,14 @@ def role_assignments(self) -> SyncRoleAssignmentsApi:
"""
return self._role_assignments

@property
def relationship_tuples(self) -> SyncRelationshipTuplesApi:
"""
API for managing relationship tuples.
See: https://api.permit.io/v2/redoc#tag/Relationship-tuples
"""
return self._relationship_tuples

@property
def roles(self) -> SyncRolesApi:
"""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def get_requirements(env=""):

setup(
name="permit",
version="2.2.0",
version="2.2.1",
packages=find_packages(),
author="Asaf Cohen",
author_email="asaf@permit.io",
Expand Down
73 changes: 73 additions & 0 deletions tests/test_rebac_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
RelationshipTupleCreate,
RelationshipTupleDelete,
ResourceCreate,
ResourceInstanceCreate,
ResourceRoleCreate,
RoleAssignmentCreate,
RoleAssignmentRemove,
Expand Down Expand Up @@ -284,6 +285,18 @@ class PermissionAssertions:
(f"{ACCOUNT.key}:cocacola", "account", f"{FOLDER.key}:recipes", TENANT_CC.key),
]

BULK_RELATIONSHIPS = [
# rnd folder contains 2 documents
(f"{FOLDER.key}:media", "parent", f"{DOCUMENT.key}:movie1", TENANT_PERMIT.key),
(f"{FOLDER.key}:media", "parent", f"{DOCUMENT.key}:movie2", TENANT_PERMIT.key),
]

BULK_RELATIONSHIPS_INSTANCES = [
f"{FOLDER.key}:media",
f"{DOCUMENT.key}:movie1",
f"{DOCUMENT.key}:movie2",
]

ASSIGNMENTS_AND_ASSERTIONS: List[PermissionAssertions] = [
# direct access
PermissionAssertions(
Expand Down Expand Up @@ -718,6 +731,66 @@ async def test_rebac_policy(permit: Permit):
assert rel_tuple.relation == relation
assert rel_tuple.object == object
assert rel_tuple.tenant == tenant

tuples = await permit.api.relationship_tuples.list()
len_tuples = len(tuples)
logger.debug(
f"there are currently {len_tuples} relationship tuples in the system"
)

# bulk create relationship tuples
bulk_relationships_to_create = [
RelationshipTupleCreate(
subject=subject, relation=relation, object=object, tenant=tenant
)
for (subject, relation, object, tenant) in BULK_RELATIONSHIPS
]
bulk_relationships_to_delete = [
RelationshipTupleDelete(subject=subject, relation=relation, object=object)
for (subject, relation, object, tenant) in BULK_RELATIONSHIPS
]

for instance_key in BULK_RELATIONSHIPS_INSTANCES:
parts = instance_key.split(":")
logger.debug(f"creating resource instance: {instance_key}")
await permit.api.resource_instances.create(
ResourceInstanceCreate(
key=parts[1], resource=parts[0], tenant=TENANT_PERMIT.key
)
)

async def create_relationships_in_bulk():
await permit.api.relationship_tuples.bulk_create(
tuples=bulk_relationships_to_create
)

tuples = await permit.api.relationship_tuples.list()
assert len(tuples) == len_tuples + len(BULK_RELATIONSHIPS)
logger.debug(
f"there are currently {len(tuples)} relationship tuples in the system"
)

async def remove_relationships_in_bulk():
await permit.api.relationship_tuples.bulk_delete(
tuples=bulk_relationships_to_delete
)

tuples = await permit.api.relationship_tuples.list()
assert len(tuples) == len_tuples
logger.debug(
f"there are currently {len(tuples)} relationship tuples in the system"
)

logger.debug(
f"creating {len(BULK_RELATIONSHIPS)} relationship tuples in bulk: {str(BULK_RELATIONSHIPS)}"
)
await create_relationships_in_bulk()

logger.debug(
f"removing the same {len(BULK_RELATIONSHIPS)} relationship tuples in bulk: {str(BULK_RELATIONSHIPS)}"
)
await remove_relationships_in_bulk()

# assign roles and then run permission checks
for test_step in ASSIGNMENTS_AND_ASSERTIONS:
try:
Expand Down

0 comments on commit fb85aa7

Please sign in to comment.