From 81dcf174b1f5cecb6012fa93cf38490bdbc553eb Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 18:45:25 +0200 Subject: [PATCH 1/6] Enforcer functions --- permit/enforcement/enforcer.py | 86 +++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 7850b69..f81a63d 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -1,6 +1,6 @@ import json from pprint import pformat -from typing import List, Optional, TypedDict, Union +from typing import Any, Dict, List, Optional, TypedDict, Union import aiohttp from aiohttp import ClientTimeout @@ -375,6 +375,84 @@ async def check( error=err, ) from err + async def get_user_permissions( + self, + user: Union[dict, str], + tenants: Optional[List[str]] = None, + resources: Optional[List[str]] = None, + resource_types: Optional[List[str]] = None, + ) -> dict: + input_data = { + "user": {"key": user} if isinstance(user, str) else user, + "tenants": tenants, + "resources": resources, + "resource_types": resource_types, + } + + async with aiohttp.ClientSession(headers=self._headers, **self._timeout_config) as session: + url = f"{self._base_url}/user-permissions" + try: + async with session.post( + url, + data=json.dumps(input_data), + ) as response: + if response.status != 200: + raise PermitConnectionError( + f"Permit.getUserPermissions() got an unexpected status code: {response.status}, " + f"please check your SDK init and make sure the PDP sidecar is configured correctly.\n" + f"Read more about setting up the PDP at {SETUP_PDP_DOCS_LINK}" + ) + + content = await response.json() + permissions = content.get("result", {}).get("permissions", {}) if "result" in content else content + + logger.debug( + f"permit.get_user_permissions() response:\n" + f"input: {pformat(input_data, indent=2)}\n" + f"response data: {pformat(permissions, indent=2)}" + ) + return permissions + + except aiohttp.ClientError as err: + logger.error(f"Error in permit.get_user_permissions(): {err}") + raise PermitConnectionError( + f"Permit SDK got error: {err}, \n" + f"and cannot connect to the PDP container, please check your configuration and make sure it's " + f"running at {self._base_url} and accepting requests. \n" + f"Read more about setting up the PDP at {SETUP_PDP_DOCS_LINK}", + error=err, + ) from err + + async def filter_objects( + self, user: User, action: Action, _context: Dict[str, str], resources: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Filter objects based on permissions using bulk check. + Port of Go's FilterObjects function. + """ + requests: List[CheckQuery] = [] + for resource in resources: + permit_resource: Dict[str, Any] = { + "type": resource.get("type"), + "key": resource.get("key"), + "context": resource.get("context", {}), + "attributes": resource.get("attributes", {}), + "tenant": resource.get("tenant"), + } + check_query: CheckQuery = { + "user": user, + "action": action, + "resource": permit_resource, + } + requests.append(check_query) + + results = await self.bulk_check(requests) + filtered_resources: List[Dict[str, Any]] = [] + for i, result in enumerate(results): + if result: + filtered_resources.append(resources[i]) + return filtered_resources + def _normalize_resource(self, resource: ResourceInput) -> ResourceInput: normalized_resource: ResourceInput = resource.copy() if normalized_resource.context is None: @@ -405,6 +483,12 @@ def _resource_from_string(resource: str) -> ResourceInput: raise ValueError(f"permit.check() got invalid resource string: {resource}") return ResourceInput(type=parts[0], key=(parts[1] if len(parts) > 1 else None)) + @staticmethod + def _user_repr(user: dict) -> str: + if user.get("attributes") or user.get("email"): + return json.dumps(user) + return user["key"] + class SyncEnforcer(Enforcer, metaclass=SyncClass): pass From 115356fc63824b4265b82ccbf90e2a829e4ddc76 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 18:46:08 +0200 Subject: [PATCH 2/6] Update permit.py --- permit/permit.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/permit/permit.py b/permit/permit.py index 386cbf4..b5025c8 100644 --- a/permit/permit.py +++ b/permit/permit.py @@ -1,6 +1,6 @@ import json from contextlib import contextmanager -from typing import Generator, List, Optional +from typing import Any, Dict, Generator, List, Optional from loguru import logger from typing_extensions import Self @@ -219,3 +219,49 @@ async def check( await permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) """ return await self._enforcer.check(user, action, resource, context) + + async def get_user_permissions( + self, + user: User, + tenants: Optional[List[str]] = None, + resources: Optional[List[str]] = None, + resource_types: Optional[List[str]] = None, + ) -> dict: + """ + Get all permissions for a user. + + Args: + user: The user object or user key + tenants: Optional list of tenants to filter permissions + resources: Optional list of resources to filter + resource_types: Optional list of resource types to filter + config: Optional configuration dictionary + + Returns: + dict: User permissions per tenant + + Raises: + PermitConnectionError: If an error occurs while sending the request to the PDP + """ + return await self._enforcer.get_user_permissions(user, tenants, resources, resource_types) + + async def filter_objects( + self, user: User, action: Action, context: Context, resources: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Get all permissions for a user. + + Args: + user: The user object or user key + tenants: Optional list of tenants to filter permissions + resources: Optional list of resources to filter + resource_types: Optional list of resource types to filter + config: Optional configuration dictionary + + Returns: + dict: User permissions per tenant + + Raises: + PermitConnectionError: If an error occurs while sending the request to the PDP + """ + return await self._enforcer.filter_objects(user, action, context, resources) From 1a802b0831f3501b0bb7157255d5fa818e7bf888 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 18:46:39 +0200 Subject: [PATCH 3/6] Update test_abac_pdp.py --- tests/test_abac_pdp.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_abac_pdp.py b/tests/test_abac_pdp.py index c320113..222ec33 100644 --- a/tests/test_abac_pdp.py +++ b/tests/test_abac_pdp.py @@ -1,3 +1,6 @@ +from typing import Any, Dict, List + +import aiohttp import pytest from permit import Permit, PermitConnectionError, TenantCreate, UserCreate @@ -27,9 +30,45 @@ async def test_abac_pdp_cloud_error(permit_cloud: Permit): "attributes": {"private": False}, }, ) + except (PermitConnectionError, aiohttp.ClientError) as error: + assert isinstance(error, PermitConnectionError) + else: + pytest.fail("Should have raised an exception") + + +async def test_get_user_permissions_cloud_error(permit_cloud: Permit): + user_test = UserCreate( + key="maya@permit.io", + email="maya@permit.io", + first_name="Maya", + last_name="Barak", + attributes={"age": 23}, + ) - except Exception as error: # noqa: BLE001 + try: + await permit_cloud.get_user_permissions( + user={"key": user_test.key, "email": user_test.email, "attributes": user_test.attributes}, + tenants=["default"], + resources=["Blog:dddddd"], + resource_types=["Blog"], + ) + except (PermitConnectionError, aiohttp.ClientError) as error: assert isinstance(error, PermitConnectionError) + else: + pytest.fail("Should have raised an exception") + +async def test_filter_objects_cloud_error(permit_cloud: Permit): + user_test = {"key": "maya@permit.io", "email": "maya@permit.io", "attributes": {"age": 23}} + + test_resources: List[Dict[str, Any]] = [ + {"type": "Blog", "key": "doc1", "context": {}, "attributes": {}, "tenant": "default"}, + {"type": "Document", "key": "doc2", "context": {}, "attributes": {}, "tenant": "default"}, + ] + + try: + await permit_cloud.filter_objects(user=user_test, action="read", context={}, resources=test_resources) + except (PermitConnectionError, aiohttp.ClientError) as error: + assert isinstance(error, PermitConnectionError) else: pytest.fail("Should have raised an exception") From 5127376f6b838deb58efa89bc402d571230ae9b9 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 22:47:37 +0200 Subject: [PATCH 4/6] Update permit/enforcement/enforcer.py --- permit/enforcement/enforcer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index f81a63d..d83b120 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -483,12 +483,5 @@ def _resource_from_string(resource: str) -> ResourceInput: raise ValueError(f"permit.check() got invalid resource string: {resource}") return ResourceInput(type=parts[0], key=(parts[1] if len(parts) > 1 else None)) - @staticmethod - def _user_repr(user: dict) -> str: - if user.get("attributes") or user.get("email"): - return json.dumps(user) - return user["key"] - - class SyncEnforcer(Enforcer, metaclass=SyncClass): pass From 930c08f72ac68c8ea54ee848c1e6f9aefa16c984 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 22:51:51 +0200 Subject: [PATCH 5/6] Update permit/enforcement/enforcer.py --- permit/enforcement/enforcer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index d83b120..8b5038c 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -483,5 +483,6 @@ def _resource_from_string(resource: str) -> ResourceInput: raise ValueError(f"permit.check() got invalid resource string: {resource}") return ResourceInput(type=parts[0], key=(parts[1] if len(parts) > 1 else None)) + class SyncEnforcer(Enforcer, metaclass=SyncClass): pass From 4488fd2b5c581d4e82c5eae47bc9ab4eaf332622 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 10 Feb 2025 23:07:19 +0200 Subject: [PATCH 6/6] Update enforcer.py --- permit/enforcement/enforcer.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 8b5038c..c72f93e 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -30,6 +30,7 @@ class CheckQuery(TypedDict): user: User action: Action resource: Resource + context: Optional[Context] SETUP_PDP_DOCS_LINK = ( @@ -424,7 +425,7 @@ async def get_user_permissions( ) from err async def filter_objects( - self, user: User, action: Action, _context: Dict[str, str], resources: List[Dict[str, Any]] + self, user: User, action: Action, context: Dict[str, str], resources: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """ Filter objects based on permissions using bulk check. @@ -439,11 +440,7 @@ async def filter_objects( "attributes": resource.get("attributes", {}), "tenant": resource.get("tenant"), } - check_query: CheckQuery = { - "user": user, - "action": action, - "resource": permit_resource, - } + check_query: CheckQuery = {"user": user, "action": action, "resource": permit_resource, "context": context} requests.append(check_query) results = await self.bulk_check(requests)