diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 7850b69..c72f93e 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 @@ -30,6 +30,7 @@ class CheckQuery(TypedDict): user: User action: Action resource: Resource + context: Optional[Context] SETUP_PDP_DOCS_LINK = ( @@ -375,6 +376,80 @@ 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, "context": context} + 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: 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) 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")