├── .editorconfig ├── .github └── workflows │ ├── pre-commit.yml │ ├── python-sdk-publish.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── imgs └── Python.png ├── permit ├── __init__.py ├── api │ ├── __init__.py │ ├── api_client.py │ ├── base.py │ ├── condition_set_rules.py │ ├── condition_sets.py │ ├── context.py │ ├── deprecated.py │ ├── elements.py │ ├── environments.py │ ├── models.py │ ├── projects.py │ ├── relationship_tuples.py │ ├── resource_action_groups.py │ ├── resource_actions.py │ ├── resource_attributes.py │ ├── resource_instances.py │ ├── resource_relations.py │ ├── resource_roles.py │ ├── resources.py │ ├── role_assignments.py │ ├── roles.py │ ├── sync_api_client.py │ ├── tenants.py │ └── users.py ├── config.py ├── enforcement │ ├── __init__.py │ ├── enforcer.py │ └── interfaces.py ├── exceptions.py ├── logger.py ├── pdp_api │ ├── __init__.py │ ├── base.py │ ├── models.py │ ├── pdp_api_client.py │ └── role_assignments.py ├── permit.py ├── sync.py └── utils │ ├── __init__.py │ ├── context.py │ ├── deprecation.py │ ├── dicts.py │ ├── pydantic_version.py │ └── sync.py ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── endpoints ├── test_bulk_operations.py ├── test_envs.py ├── test_error_response.py ├── test_resources.py ├── test_resources_sync.py ├── test_role_assignments.py ├── test_roles.py └── test_users_tenants.py ├── test_abac_e2e.py ├── test_abac_pdp.py ├── test_rbac_e2e.py ├── test_rbac_e2e_sync.py ├── test_rebac_e2e.py ├── test_sync_client.py └── utils.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | insert_final_newline=false 7 | indent_style=space 8 | indent_size=2 9 | trim_trailing_whitespace=true 10 | 11 | [*.py] 12 | indent_size=4 13 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master, main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | - uses: pre-commit/action@v3.0.1 15 | -------------------------------------------------------------------------------- /.github/workflows/python-sdk-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release permit python SDK 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | PROJECT_ID: 7f55831d77c642739bc17733ab0af138 #github actions project id (under 'Permit.io Tests' workspace) 9 | ENV_NAME: python-sdk-ci 10 | 11 | jobs: 12 | publish_python_sdk: 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: pypi 16 | url: https://pypi.org/p/permit 17 | permissions: 18 | id-token: write 19 | contents: write # 'write' access to repository contents 20 | pull-requests: write # 'write' access to pull requests 21 | steps: 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Python setup 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.11.8' 30 | 31 | - name: Bump version and commit changes 32 | run: | 33 | sed -i "s/version=\"[0-9.]*\"/version=\"${{ github.event.release.tag_name }}\"/" setup.py 34 | 35 | - name: Build Python package 36 | run: | 37 | pip install wheel 38 | python setup.py sdist bdist_wheel 39 | 40 | - name: Publish package distributions to PyPI 41 | uses: pypa/gh-action-pypi-publish@release/v1 42 | with: 43 | password: ${{ secrets.PYPI_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release permit python SDK 2 | 3 | on: 4 | release: 5 | # job will automatically run after a new "release" is create on github. 6 | types: [created] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: [] 10 | 11 | 12 | jobs: 13 | build-n-publish: 14 | name: Build and publish permit python SDK to PyPI and TestPyPI 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: "3.9" 22 | - name: Install python deps 23 | run: >- 24 | python -m pip install build twine wheel --user 25 | - name: Build & Publish SDK 26 | run: >- 27 | make publish 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - master 7 | push: 8 | branches: 9 | - main 10 | - master 11 | 12 | env: 13 | PROJECT_ID: 7f55831d77c642739bc17733ab0af138 #github actions project id (under 'Permit.io Tests' workspace) 14 | ENV_NAME: python-sdk-ci 15 | 16 | jobs: 17 | pytest: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | pydantic-version: ['pydantic<2.0.0', 'pydantic>=2.0.0'] 23 | name: pytest (Pydantic ${{ matrix.pydantic-version }}) 24 | services: 25 | pdp: 26 | image: permitio/pdp-v2:latest 27 | ports: 28 | - 7766:7000 29 | env: 30 | PDP_API_KEY: ${{ secrets.PROJECT_API_KEY }} 31 | PDP_DEBUG: true 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | 36 | - name: Python setup 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: '3.11.8' 40 | 41 | - name: Creation env ${{ env.ENV_NAME }}-${{ github.run_id }}-${{ matrix.pydantic-version }} 42 | id: create_env 43 | run: | 44 | ENV_KEY="${{ env.ENV_NAME }}-${{ github.run_id }}-${{ matrix.pydantic-version == 'pydantic<2.0.0' && 'v1' || 'v2' }}" 45 | echo "ENV_KEY=$ENV_KEY" >> $GITHUB_ENV 46 | 47 | response=$(curl -X POST \ 48 | https://api.permit.io/v2/projects/${{ env.PROJECT_ID }}/envs \ 49 | -H 'Authorization: Bearer ${{ secrets.PROJECT_API_KEY }}' \ 50 | -H 'Content-Type: application/json' \ 51 | -d '{ 52 | "key": "'"$ENV_KEY"'", 53 | "name": "'"$ENV_KEY"'" 54 | }') 55 | 56 | # Extract the new env id 57 | echo "ENV_ID=$(echo "$response" | jq -r '.id')" >> $GITHUB_ENV 58 | 59 | echo "New env ID: $ENV_ID with key: $ENV_KEY" 60 | 61 | - name: Fetch API_KEY of ${{ env.ENV_KEY }} 62 | run: | 63 | response=$(curl -X GET \ 64 | https://api.permit.io/v2/api-key/${{ env.PROJECT_ID }}/${{ env.ENV_ID }} \ 65 | -H 'Authorization: Bearer ${{ secrets.PROJECT_API_KEY }}') 66 | 67 | # Extract the secret from the response which is the API_KEY of the new env 68 | echo "ENV_API_KEY=$(echo "$response" | jq -r '.secret')" >> $GITHUB_ENV 69 | 70 | echo "New env api key: $ENV_API_KEY" 71 | 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install --upgrade pip 75 | pip install flake8 pytest pytest-cov 76 | # Pin pydantic version according to matrix 77 | pip install "${{ matrix.pydantic-version }}" 78 | # Explicitly install email-validator which is required for Pydantic email validation 79 | pip install email-validator 80 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 81 | if [ -f requirements.txt ]; then pip install -r requirements.txt --no-deps; fi 82 | 83 | - name: Show installed packages 84 | run: pip list 85 | 86 | - name: Test with pytest 87 | env: 88 | PDP_URL: http://localhost:7766 89 | API_TIER: prod 90 | ORG_PDP_API_KEY: ${{ env.ENV_API_KEY }} 91 | PROJECT_PDP_API_KEY: ${{ env.ENV_API_KEY }} 92 | PDP_API_KEY: ${{ env.ENV_API_KEY }} 93 | run: | 94 | pytest -s --cache-clear tests/ 95 | 96 | - name: Delete env ${{ env.ENV_KEY }} 97 | if: always() 98 | run: | 99 | curl -X DELETE \ 100 | https://api.permit.io/v2/projects/${{ env.PROJECT_ID }}/envs/${{ env.ENV_ID }} \ 101 | -H 'Authorization: Bearer ${{ secrets.PROJECT_API_KEY }}' 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # editors 132 | .vscode/ 133 | .DS_Store # macOS 134 | .idea/ 135 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile=black 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-added-large-files 8 | - id: check-case-conflict 9 | - id: check-executables-have-shebangs 10 | - id: check-json 11 | - id: check-toml 12 | - id: check-yaml 13 | - id: check-xml 14 | - id: check-merge-conflict 15 | - id: mixed-line-ending 16 | args: [ --fix=lf ] 17 | 18 | - repo: https://github.com/astral-sh/ruff-pre-commit 19 | rev: v0.6.9 20 | hooks: 21 | - id: ruff 22 | args: [--fix] 23 | files: \.py$ 24 | types: [ file ] 25 | - id: ruff-format 26 | files: \.py$ 27 | types: [ file ] 28 | 29 | - repo: https://github.com/pre-commit/mirrors-mypy 30 | rev: v1.11.2 31 | hooks: 32 | - id: mypy 33 | pass_filenames: false 34 | additional_dependencies: 35 | - pydantic 36 | files: \.py$ 37 | types: [ file ] 38 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md requirements.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | generate-models: 6 | datamodel-codegen --url https://api.permit.io/v2/openapi.json \ 7 | --input-file-type openapi \ 8 | --output permit/api/models.py \ 9 | --output-model-type pydantic.BaseModel \ 10 | --allow-extra-fields \ 11 | --enum-field-as-literal one \ 12 | --use-one-literal-as-default \ 13 | --use-subclass-enum 14 | 15 | # python packages (pypi) 16 | clean: 17 | rm -rf *.egg-info build/ dist/ 18 | 19 | publish: 20 | $(MAKE) clean 21 | python setup.py sdist bdist_wheel 22 | python -m twine upload dist/* 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python.png](imgs/Python.png) 2 | # Permit.io Python SDK 3 | 4 | Python SDK for interacting with the Permit.io full-stack permissions platform. 5 | 6 | ## Installation 7 | 8 | ```py 9 | pip install permit 10 | ``` 11 | 12 | ## Documentation 13 | 14 | [Read the documentation at Permit.io website](https://docs.permit.io/sdk/python/quickstart-python) 15 | -------------------------------------------------------------------------------- /imgs/Python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/imgs/Python.png -------------------------------------------------------------------------------- /permit/__init__.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F401 2 | from .api.models import * # noqa: F403 3 | from .config import PermitConfig 4 | from .enforcement.enforcer import Action, Resource, User 5 | from .enforcement.interfaces import ( 6 | AssignedRole, 7 | AuthorizedUsersResult, 8 | ResourceInput, 9 | UserInput, 10 | ) 11 | from .exceptions import ( 12 | PermitAlreadyExistsError, 13 | PermitApiDetailedError, 14 | PermitApiError, 15 | PermitConnectionError, 16 | PermitContextChangeError, 17 | PermitContextError, 18 | PermitError, 19 | PermitException, 20 | PermitNotFoundError, 21 | PermitValidationError, 22 | ) 23 | from .permit import Permit 24 | from .utils.context import Context 25 | -------------------------------------------------------------------------------- /permit/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/permit/api/__init__.py -------------------------------------------------------------------------------- /permit/api/api_client.py: -------------------------------------------------------------------------------- 1 | from ..config import PermitConfig 2 | from .condition_set_rules import ConditionSetRulesApi 3 | from .condition_sets import ConditionSetsApi 4 | from .deprecated import DeprecatedApi 5 | from .environments import EnvironmentsApi 6 | from .projects import ProjectsApi 7 | from .relationship_tuples import RelationshipTuplesApi 8 | from .resource_action_groups import ResourceActionGroupsApi 9 | from .resource_actions import ResourceActionsApi 10 | from .resource_attributes import ResourceAttributesApi 11 | from .resource_instances import ResourceInstancesApi 12 | from .resource_relations import ResourceRelationsApi 13 | from .resource_roles import ResourceRolesApi 14 | from .resources import ResourcesApi 15 | from .role_assignments import RoleAssignmentsApi 16 | from .roles import RolesApi 17 | from .tenants import TenantsApi 18 | from .users import UsersApi 19 | 20 | 21 | class PermitApiClient(DeprecatedApi): 22 | def __init__(self, config: PermitConfig): 23 | """ 24 | Constructs a new instance of the ApiClient class with the specified SDK configuration. 25 | 26 | Args: 27 | config: The configuration for the Permit SDK. 28 | """ 29 | super().__init__(config) 30 | 31 | self._condition_set_rules = ConditionSetRulesApi(config) 32 | self._condition_sets = ConditionSetsApi(config) 33 | self._environments = EnvironmentsApi(config) 34 | self._projects = ProjectsApi(config) 35 | self._action_groups = ResourceActionGroupsApi(config) 36 | self._resource_actions = ResourceActionsApi(config) 37 | self._resource_attributes = ResourceAttributesApi(config) 38 | self._resource_roles = ResourceRolesApi(config) 39 | self._resource_relations = ResourceRelationsApi(config) 40 | self._resource_instances = ResourceInstancesApi(config) 41 | self._resources = ResourcesApi(config) 42 | self._role_assignments = RoleAssignmentsApi(config) 43 | self._relationship_tuples = RelationshipTuplesApi(config) 44 | self._roles = RolesApi(config) 45 | self._tenants = TenantsApi(config) 46 | self._users = UsersApi(config) 47 | 48 | @property 49 | def condition_set_rules(self) -> ConditionSetRulesApi: 50 | """ 51 | API for managing condition set rules. 52 | See: https://api.permit.io/v2/redoc#tag/Condition-Set-Rules 53 | """ 54 | return self._condition_set_rules 55 | 56 | @property 57 | def condition_sets(self) -> ConditionSetsApi: 58 | """ 59 | API for managing condition sets. 60 | See: https://api.permit.io/v2/redoc#tag/Condition-Sets 61 | """ 62 | return self._condition_sets 63 | 64 | @property 65 | def projects(self) -> ProjectsApi: 66 | """ 67 | API for managing projects. 68 | See: https://api.permit.io/v2/redoc#tag/Projects 69 | """ 70 | return self._projects 71 | 72 | @property 73 | def environments(self) -> EnvironmentsApi: 74 | """ 75 | API for managing environments. 76 | See: https://api.permit.io/v2/redoc#tag/Environments 77 | """ 78 | return self._environments 79 | 80 | @property 81 | def action_groups(self) -> ResourceActionGroupsApi: 82 | """ 83 | API for managing resource action groups. 84 | See: https://api.permit.io/v2/redoc#tag/Resource-Action-Groups 85 | """ 86 | return self._action_groups 87 | 88 | @property 89 | def resource_actions(self) -> ResourceActionsApi: 90 | """ 91 | API for managing resource actions. 92 | See: https://api.permit.io/v2/redoc#tag/Resource-Actions 93 | """ 94 | return self._resource_actions 95 | 96 | @property 97 | def resource_attributes(self) -> ResourceAttributesApi: 98 | """ 99 | API for managing resource attributes. 100 | See: https://api.permit.io/v2/redoc#tag/Resource-Attributes 101 | """ 102 | return self._resource_attributes 103 | 104 | @property 105 | def resource_roles(self) -> ResourceRolesApi: 106 | """ 107 | API for managing resource roles. 108 | See: https://api.permit.io/v2/redoc#tag/Resource-Roles 109 | """ 110 | return self._resource_roles 111 | 112 | @property 113 | def resource_relations(self) -> ResourceRelationsApi: 114 | """ 115 | API for managing resource relations. 116 | See: https://api.permit.io/v2/redoc#tag/Resource-Relations 117 | """ 118 | return self._resource_relations 119 | 120 | @property 121 | def resource_instances(self) -> ResourceInstancesApi: 122 | """ 123 | API for managing resource instances. 124 | See: https://api.permit.io/v2/redoc#tag/Resource-Instances 125 | """ 126 | return self._resource_instances 127 | 128 | @property 129 | def resources(self) -> ResourcesApi: 130 | """ 131 | API for managing resources. 132 | See: https://api.permit.io/v2/redoc#tag/Resources 133 | """ 134 | return self._resources 135 | 136 | @property 137 | def role_assignments(self) -> RoleAssignmentsApi: 138 | """ 139 | API for managing role assignments. 140 | See: https://api.permit.io/v2/redoc#tag/Role-Assignments 141 | """ 142 | return self._role_assignments 143 | 144 | @property 145 | def relationship_tuples(self) -> RelationshipTuplesApi: 146 | """ 147 | API for managing relationship tuples. 148 | See: https://api.permit.io/v2/redoc#tag/Relationship-tuples 149 | """ 150 | return self._relationship_tuples 151 | 152 | @property 153 | def roles(self) -> RolesApi: 154 | """ 155 | API for managing roles. 156 | See: https://api.permit.io/v2/redoc#tag/Roles 157 | """ 158 | return self._roles 159 | 160 | @property 161 | def tenants(self) -> TenantsApi: 162 | """ 163 | API for managing tenants. 164 | See: https://api.permit.io/v2/redoc#tag/Tenants 165 | """ 166 | return self._tenants 167 | 168 | @property 169 | def users(self) -> UsersApi: 170 | """ 171 | API for managing users. 172 | See: https://api.permit.io/v2/redoc#tag/Users 173 | """ 174 | return self._users 175 | -------------------------------------------------------------------------------- /permit/api/condition_set_rules.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ConditionSetRuleCreate, ConditionSetRuleRead, ConditionSetRuleRemove 17 | 18 | 19 | class ConditionSetRulesApi(BasePermitApi): 20 | @property 21 | def __condition_set_rules(self) -> SimpleHttpClient: 22 | return self._build_http_client( 23 | f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/set_rules" 24 | ) 25 | 26 | @validate_arguments # type: ignore[operator] 27 | async def list( 28 | self, 29 | user_set_key: Optional[str] = None, 30 | permission_key: Optional[str] = None, 31 | resource_set_key: Optional[str] = None, 32 | page: int = 1, 33 | per_page: int = 100, 34 | ) -> List[ConditionSetRuleRead]: 35 | """ 36 | Retrieves a list of condition set rule rules. 37 | 38 | Args: 39 | user_set_key: the key of the userset, if used only rules matching that userset will be fetched. 40 | permission_key: the key of the permission, formatted as :. 41 | if used, only rules granting that permission will be fetched. 42 | resource_set_key: the key of the resourceset, if used only rules matching that resourceset will be fetched. 43 | page: The page number to fetch (default: 1). 44 | per_page: How many items to fetch per page (default: 100). 45 | 46 | Returns: 47 | an array of condition set rule rules. 48 | 49 | Raises: 50 | PermitApiError: If the API returns an error HTTP status code. 51 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 52 | """ 53 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 54 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 55 | params = pagination_params(page, per_page) 56 | if user_set_key is not None: 57 | params.update(user_set=user_set_key) 58 | if permission_key is not None: 59 | params.update(permission=permission_key) 60 | if resource_set_key is not None: 61 | params.update(resource_set=resource_set_key) 62 | return await self.__condition_set_rules.get( 63 | "", 64 | model=List[ConditionSetRuleRead], 65 | params=params, 66 | ) 67 | 68 | @validate_arguments # type: ignore[operator] 69 | async def create(self, rule: ConditionSetRuleCreate) -> List[ConditionSetRuleRead]: 70 | """ 71 | Creates a new condition set rule. 72 | 73 | Args: 74 | rule: The condition set rule to create. 75 | 76 | Returns: 77 | the created condition set rule. 78 | 79 | Raises: 80 | PermitApiError: If the API returns an error HTTP status code. 81 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 82 | """ 83 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 84 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 85 | return await self.__condition_set_rules.post("", model=List[ConditionSetRuleRead], json=rule) 86 | 87 | @validate_arguments # type: ignore[operator] 88 | async def delete(self, rule: ConditionSetRuleRemove) -> None: 89 | """ 90 | Deletes a condition set rule. 91 | 92 | Args: 93 | rule: The condition set rule to delete. 94 | 95 | Raises: 96 | PermitApiError: If the API returns an error HTTP status code. 97 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 98 | """ 99 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 100 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 101 | return await self.__condition_set_rules.delete("", json=rule) 102 | -------------------------------------------------------------------------------- /permit/api/condition_sets.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ConditionSetCreate, ConditionSetRead, ConditionSetUpdate 17 | 18 | 19 | class ConditionSetsApi(BasePermitApi): 20 | @property 21 | def __condition_sets(self) -> SimpleHttpClient: 22 | return self._build_http_client( 23 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/condition_sets" 24 | ) 25 | 26 | @validate_arguments # type: ignore[operator] 27 | async def list(self, page: int = 1, per_page: int = 100) -> List[ConditionSetRead]: 28 | """ 29 | Retrieves a list of condition sets. 30 | 31 | Args: 32 | page: The page number to fetch (default: 1). 33 | per_page: How many items to fetch per page (default: 100). 34 | 35 | Returns: 36 | an array of condition sets. 37 | 38 | Raises: 39 | PermitApiError: If the API returns an error HTTP status code. 40 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 41 | """ 42 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 43 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 44 | return await self.__condition_sets.get( 45 | "", model=List[ConditionSetRead], params=pagination_params(page, per_page) 46 | ) 47 | 48 | async def _get(self, condition_set_key: str) -> ConditionSetRead: 49 | return await self.__condition_sets.get(f"/{condition_set_key}", model=ConditionSetRead) 50 | 51 | @validate_arguments # type: ignore[operator] 52 | async def get(self, condition_set_key: str) -> ConditionSetRead: 53 | """ 54 | Retrieves a condition set by its key. 55 | 56 | Args: 57 | condition_set_key: The key of the condition set. 58 | 59 | Returns: 60 | the condition set. 61 | 62 | Raises: 63 | PermitApiError: If the API returns an error HTTP status code. 64 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 65 | """ 66 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 67 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 68 | return await self._get(condition_set_key) 69 | 70 | @validate_arguments # type: ignore[operator] 71 | async def get_by_key(self, condition_set_key: str) -> ConditionSetRead: 72 | """ 73 | Retrieves a condition set by its key. 74 | Alias for the get method. 75 | 76 | Args: 77 | condition_set_key: The key of the condition set. 78 | 79 | Returns: 80 | the condition set. 81 | 82 | Raises: 83 | PermitApiError: If the API returns an error HTTP status code. 84 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 85 | """ 86 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 87 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 88 | return await self._get(condition_set_key) 89 | 90 | @validate_arguments # type: ignore[operator] 91 | async def get_by_id(self, condition_set_id: str) -> ConditionSetRead: 92 | """ 93 | Retrieves a condition set by its ID. 94 | Alias for the get method. 95 | 96 | Args: 97 | condition_set_id: The ID of the condition set. 98 | 99 | Returns: 100 | the condition set. 101 | 102 | Raises: 103 | PermitApiError: If the API returns an error HTTP status code. 104 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 105 | """ 106 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 107 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 108 | return await self._get(condition_set_id) 109 | 110 | @validate_arguments # type: ignore[operator] 111 | async def create(self, condition_set_data: ConditionSetCreate) -> ConditionSetRead: 112 | """ 113 | Creates a new condition set. 114 | 115 | Args: 116 | condition_set_data: The data for the new condition set. 117 | 118 | Returns: 119 | the created condition set. 120 | 121 | Raises: 122 | PermitApiError: If the API returns an error HTTP status code. 123 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 124 | """ 125 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 126 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 127 | return await self.__condition_sets.post("", model=ConditionSetRead, json=condition_set_data) 128 | 129 | @validate_arguments # type: ignore[operator] 130 | async def update(self, condition_set_key: str, condition_set_data: ConditionSetUpdate) -> ConditionSetRead: 131 | """ 132 | Updates a condition set. 133 | 134 | Args: 135 | condition_set_key: The key of the condition set. 136 | condition_set_data: The updated data for the condition set. 137 | 138 | Returns: 139 | the updated condition set. 140 | 141 | Raises: 142 | PermitApiError: If the API returns an error HTTP status code. 143 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 144 | """ 145 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 146 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 147 | return await self.__condition_sets.patch( 148 | f"/{condition_set_key}", 149 | model=ConditionSetRead, 150 | json=condition_set_data, 151 | ) 152 | 153 | @validate_arguments # type: ignore[operator] 154 | async def delete(self, condition_set_key: str) -> None: 155 | """ 156 | Deletes a condition set. 157 | 158 | Args: 159 | condition_set_key: The key of the condition set to delete. 160 | 161 | Raises: 162 | PermitApiError: If the API returns an error HTTP status code. 163 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 164 | """ 165 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 166 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 167 | return await self.__condition_sets.delete(f"/{condition_set_key}") 168 | -------------------------------------------------------------------------------- /permit/api/context.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | from loguru import logger 5 | 6 | from ..exceptions import PermitContextChangeError 7 | 8 | 9 | class ApiKeyAccessLevel(str, Enum): 10 | """ 11 | The `ApiKeyAccessLevel` enum represents the access level of a Permit API Key. 12 | """ 13 | 14 | WAIT_FOR_INIT = "WAIT_FOR_INIT" 15 | """ 16 | Wait for initialization of the API key. 17 | """ 18 | 19 | ORGANIZATION_LEVEL_API_KEY = "ORGANIZATION_LEVEL_API_KEY" 20 | """ 21 | This type of API Key will allow the SDK user to modify all projects and 22 | environments under the granted organization (workspace). 23 | """ 24 | 25 | PROJECT_LEVEL_API_KEY = "PROJECT_LEVEL_API_KEY" 26 | """ 27 | This type of API Key will allow the SDK user to modify 28 | a single project and the environments under that project. 29 | """ 30 | 31 | ENVIRONMENT_LEVEL_API_KEY = "ENVIRONMENT_LEVEL_API_KEY" 32 | """ 33 | This type of API Key will allow the SDK user to modify a single Permit environment. 34 | """ 35 | 36 | 37 | API_ACCESS_LEVELS = [ 38 | ApiKeyAccessLevel.ORGANIZATION_LEVEL_API_KEY, 39 | ApiKeyAccessLevel.PROJECT_LEVEL_API_KEY, 40 | ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY, 41 | ] 42 | 43 | 44 | class ApiKeyLevel(str, Enum): 45 | """ 46 | Deprecated: `ApiKeyLevel` had a confusing name, use `ApiKeyAccessLevel` instead. 47 | """ 48 | 49 | WAIT_FOR_INIT = "WAIT_FOR_INIT" 50 | ORGANIZATION_LEVEL_API_KEY = "ORGANIZATION_LEVEL_API_KEY" 51 | PROJECT_LEVEL_API_KEY = "PROJECT_LEVEL_API_KEY" 52 | ENVIRONMENT_LEVEL_API_KEY = "ENVIRONMENT_LEVEL_API_KEY" 53 | 54 | 55 | class ApiContextLevel(int, Enum): 56 | """ 57 | The `ApiContextLevel` enum represents the context level in which the SDK is running. 58 | """ 59 | 60 | WAIT_FOR_INIT = 0 61 | """ 62 | Signifies that the context is not set yet. 63 | """ 64 | 65 | ORGANIZATION = 1 66 | """ 67 | When running in this context level, the SDK knows the current organization. 68 | """ 69 | 70 | PROJECT = 2 71 | """ 72 | When running in this context level, the SDK knows the current organization and project. 73 | """ 74 | 75 | ENVIRONMENT = 3 76 | """ 77 | When running in this context level, the SDK knows the current organization, project and environment. 78 | """ 79 | 80 | 81 | class ApiContext: 82 | """ 83 | The `ApiContext` class represents the required known context for an API method. 84 | 85 | Since the Permit API hierarchy is deeply nested, it is less convenient to specify 86 | the full object hierarchy in every request. 87 | 88 | For example, in order to list roles, the user need to specify the (id or key) of the: 89 | - the org 90 | - the project 91 | - then environment 92 | in which the roles are located under. 93 | 94 | Instead, the SDK can "remember" the current context and "auto-complete" the details 95 | from that context. 96 | 97 | We then get this kind of experience: 98 | ``` 99 | await permit.api.roles.list() 100 | ``` 101 | 102 | we can only run this function if the current context already knows the org, project 103 | and environments that we want to run under, and that is why this method assumes 104 | we are running under a `ApiContextLevel.ENVIRONMENT` context. 105 | """ 106 | 107 | def __init__(self): 108 | self._permitted_access_level = ApiKeyAccessLevel.WAIT_FOR_INIT 109 | # org, project and environment the API Key is allowed to access 110 | self._permitted_organization = None 111 | self._permitted_project = None 112 | self._permitted_environment = None 113 | 114 | # current known context 115 | self._context_level = ApiContextLevel.WAIT_FOR_INIT 116 | self._organization = None 117 | self._project = None 118 | self._environment = None 119 | 120 | def _save_api_key_accessible_scope( 121 | self, org: str, project: Optional[str] = None, environment: Optional[str] = None 122 | ): 123 | """Do not call this method directly!""" 124 | self._permitted_organization = org # cannot be none 125 | 126 | if project is not None and environment is not None: 127 | self._permitted_project = project 128 | self._permitted_environment = environment 129 | self._permitted_access_level = ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY 130 | elif project is not None: 131 | self._permitted_project = project 132 | self._permitted_environment = None 133 | self._permitted_access_level = ApiKeyAccessLevel.PROJECT_LEVEL_API_KEY 134 | else: 135 | self._permitted_project = None 136 | self._permitted_environment = None 137 | self._permitted_access_level = ApiKeyAccessLevel.ORGANIZATION_LEVEL_API_KEY 138 | 139 | @property 140 | def permitted_access_level(self) -> ApiKeyAccessLevel: 141 | """ 142 | Get the current API key level. 143 | 144 | Returns: 145 | The current API key level. 146 | """ 147 | return self._permitted_access_level 148 | 149 | @property 150 | def level(self) -> ApiContextLevel: 151 | """ 152 | Get the current SDK context level. 153 | 154 | Returns: 155 | The current SDK context level. 156 | """ 157 | return self._context_level 158 | 159 | @property 160 | def organization(self) -> Optional[str]: 161 | """ 162 | Get the current organization from the SDK context or None if unset. 163 | 164 | Returns: 165 | The current organization in the context. 166 | """ 167 | return self._organization 168 | 169 | @property 170 | def project(self) -> Optional[str]: 171 | """ 172 | Get the current project from the SDK context or None if unset. 173 | 174 | Returns: 175 | The current project in the context. 176 | """ 177 | return self._project 178 | 179 | @property 180 | def environment(self) -> Optional[str]: 181 | """ 182 | Get the current environment from the SDK context or None if unset. 183 | 184 | Returns: 185 | The current environment in the context. 186 | """ 187 | return self._environment 188 | 189 | def __verify_can_access_org(self, org: str): 190 | if org != self._permitted_organization: 191 | raise PermitContextChangeError( 192 | f"You cannot set an SDK context with org '{org}' due to insufficient API Key permissions" 193 | ) 194 | 195 | def __verify_can_access_project(self, org: str, project: str): 196 | self.__verify_can_access_org(org) 197 | if self._permitted_project is not None and project != self._permitted_project: 198 | raise PermitContextChangeError( 199 | f"You cannot set an SDK context with project '{project}' due to insufficient API Key permissions" 200 | ) 201 | 202 | def __verify_can_access_environment(self, org: str, project: str, environment: str): 203 | self.__verify_can_access_project(org, project) 204 | if self._permitted_environment is not None and environment != self._permitted_environment: 205 | raise PermitContextChangeError( 206 | f"You cannot set an SDK context with environment '{environment}' " 207 | f"due to insufficient API Key permissions" 208 | ) 209 | 210 | def set_organization_level_context(self, org: str): 211 | """ 212 | Set the current context of the SDK to a specific organization. 213 | 214 | Args: 215 | org: The organization key. 216 | """ 217 | self.__verify_can_access_org(org) 218 | logger.debug(f"Setting organization level context: {org}") 219 | self._context_level = ApiContextLevel.ORGANIZATION 220 | self._organization = org 221 | self._project = None 222 | self._environment = None 223 | 224 | def set_project_level_context(self, org: str, project: str): 225 | """ 226 | Set the current context of the SDK to a specific organization and project. 227 | 228 | Args: 229 | org: The organization key. 230 | project: The project key. 231 | """ 232 | self.__verify_can_access_project(org, project) 233 | logger.debug(f"Setting project level context: {org}/{project}") 234 | self._context_level = ApiContextLevel.PROJECT 235 | self._organization = org 236 | self._project = project 237 | self._environment = None 238 | 239 | def set_environment_level_context(self, org: str, project: str, environment: str): 240 | """ 241 | Set the current context of the SDK to a specific organization, project and environment. 242 | 243 | Args: 244 | org: The organization key. 245 | project: The project key. 246 | environment: The environment key. 247 | """ 248 | self.__verify_can_access_environment(org, project, environment) 249 | logger.debug(f"Setting environment level context: {org}/{project}/{environment}") 250 | self._context_level = ApiContextLevel.ENVIRONMENT 251 | self._organization = org 252 | self._project = project 253 | self._environment = environment 254 | -------------------------------------------------------------------------------- /permit/api/deprecated.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | from uuid import UUID 3 | 4 | from ..config import PermitConfig 5 | from ..utils.deprecation import deprecated 6 | from .base import BasePermitApi 7 | from .elements import ElementsApi, EmbeddedLoginRequestOutput 8 | from .models import ( 9 | ResourceCreate, 10 | ResourceRead, 11 | ResourceUpdate, 12 | RoleAssignmentCreate, 13 | RoleAssignmentRead, 14 | RoleAssignmentRemove, 15 | RoleCreate, 16 | RoleRead, 17 | RoleUpdate, 18 | TenantCreate, 19 | TenantRead, 20 | TenantUpdate, 21 | UserCreate, 22 | UserRead, 23 | ) 24 | from .resources import ResourcesApi 25 | from .role_assignments import RoleAssignmentsApi 26 | from .roles import RolesApi 27 | from .tenants import TenantsApi 28 | from .users import UsersApi 29 | 30 | 31 | class DeprecatedApi(BasePermitApi): 32 | """ 33 | Represents the interface for managing roles. 34 | """ 35 | 36 | def __init__(self, config: PermitConfig): 37 | super().__init__(config) 38 | self.__resources = ResourcesApi(config) 39 | self.__role_assignments = RoleAssignmentsApi(config) 40 | self.__roles = RolesApi(config) 41 | self.__tenants = TenantsApi(config) 42 | self.__users = UsersApi(config) 43 | self.__elements = ElementsApi(config) 44 | 45 | @deprecated("use permit.api.users.get() instead") 46 | async def get_user(self, user_key: str) -> UserRead: 47 | return await self.__users.get(user_key) 48 | 49 | @deprecated("use permit.api.roles.get() instead") 50 | async def get_role(self, role_key: str) -> RoleRead: 51 | return await self.__roles.get(role_key) 52 | 53 | @deprecated("use permit.api.tenants.get() instead") 54 | async def get_tenant(self, tenant_key: str) -> TenantRead: 55 | return await self.__tenants.get(tenant_key) 56 | 57 | @deprecated("use permit.api.users.get_assigned_roles() instead") 58 | async def get_assigned_roles( 59 | self, 60 | user_key: str, 61 | tenant_key: Optional[str], 62 | page: int = 1, 63 | per_page: int = 100, 64 | ) -> List[RoleAssignmentRead]: 65 | return await self.__users.get_assigned_roles(user_key, tenant=tenant_key, page=page, per_page=per_page) 66 | 67 | @deprecated("use permit.api.resources.get() instead") 68 | async def get_resource(self, resource_key: str) -> ResourceRead: 69 | return await self.__resources.get(resource_key) 70 | 71 | @deprecated("use permit.api.roles.list() instead") 72 | async def list_roles(self, page: int = 1, per_page: int = 100) -> List[RoleRead]: 73 | return await self.__roles.list(page=page, per_page=per_page) 74 | 75 | @deprecated("use permit.api.users.sync() instead") 76 | async def sync_user(self, user: Union[UserCreate, dict]) -> UserRead: 77 | return await self.__users.sync(user) 78 | 79 | @deprecated("use permit.api.users.delete() instead") 80 | async def delete_user(self, user_key: str) -> None: 81 | return await self.__users.delete(user_key) 82 | 83 | @deprecated("use permit.api.tenants.list() instead") 84 | async def list_tenants(self, page: int = 1, per_page: int = 100) -> List[TenantRead]: 85 | return await self.__tenants.list(page=page, per_page=per_page) 86 | 87 | @deprecated("use permit.api.tenants.create() instead") 88 | async def create_tenant(self, tenant: Union[TenantCreate, dict]) -> TenantRead: 89 | tenant_data = tenant if isinstance(tenant, TenantCreate) else TenantCreate(**tenant) 90 | return await self.__tenants.create(tenant_data) 91 | 92 | @deprecated("use permit.api.tenants.update() instead") 93 | async def update_tenant(self, tenant_key: str, tenant: Union[TenantUpdate, dict]) -> TenantRead: 94 | tenant_data = tenant if isinstance(tenant, TenantUpdate) else TenantUpdate(**tenant) 95 | return await self.__tenants.update(tenant_key, tenant_data) 96 | 97 | @deprecated("use permit.api.tenants.delete() instead") 98 | async def delete_tenant(self, tenant_key: str) -> None: 99 | return await self.__tenants.delete(tenant_key) 100 | 101 | @deprecated("use permit.api.roles.create() instead") 102 | async def create_role(self, role: Union[RoleCreate, dict]) -> RoleRead: 103 | role_data = role if isinstance(role, RoleCreate) else RoleCreate(**role) 104 | return await self.__roles.create(role_data) 105 | 106 | @deprecated("use permit.api.roles.update() instead") 107 | async def update_role(self, role_key: str, role: Union[RoleUpdate, dict]) -> RoleRead: 108 | role_data = role if isinstance(role, RoleUpdate) else RoleUpdate(**role) 109 | return await self.__roles.update(role_key, role_data) 110 | 111 | @deprecated("use permit.api.users.assign_role() instead") 112 | async def assign_role(self, user_key: str, role_key: str, tenant_key: str) -> RoleAssignmentRead: 113 | return await self.__role_assignments.assign( 114 | RoleAssignmentCreate(user=user_key, role=role_key, tenant=tenant_key) 115 | ) 116 | 117 | @deprecated("use permit.api.users.unassign_role() instead") 118 | async def unassign_role(self, user_key: str, role_key: str, tenant_key: str) -> None: 119 | return await self.__role_assignments.unassign( 120 | RoleAssignmentRemove(user=user_key, role=role_key, tenant=tenant_key) 121 | ) 122 | 123 | @deprecated("use permit.api.roles.delete() instead") 124 | async def delete_role(self, role_key: str): 125 | return await self.__roles.delete(role_key) 126 | 127 | @deprecated("use permit.api.resources.create() instead") 128 | async def create_resource(self, resource: Union[ResourceCreate, dict]) -> ResourceRead: 129 | resource_data = resource if isinstance(resource, ResourceCreate) else ResourceCreate(**resource) 130 | return await self.__resources.create(resource_data) 131 | 132 | @deprecated("use permit.api.resources.update() instead") 133 | async def update_resource(self, resource_key: str, resource: Union[ResourceUpdate, dict]) -> ResourceRead: 134 | resource_data = resource if isinstance(resource, ResourceUpdate) else ResourceUpdate(**resource) 135 | return await self.__resources.update(resource_key, resource_data) 136 | 137 | @deprecated("use permit.api.resources.delete() instead") 138 | async def delete_resource(self, resource_key: str): 139 | return await self.__resources.delete(resource_key) 140 | 141 | @deprecated("use permit.elements.login_as() instead") 142 | async def elements_login_as( 143 | self, user_id: Union[str, UUID], tenant_id: Union[str, UUID] 144 | ) -> EmbeddedLoginRequestOutput: 145 | return await self.__elements.login_as(user_id=user_id, tenant_id=tenant_id) 146 | -------------------------------------------------------------------------------- /permit/api/elements.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional, Union 3 | from uuid import UUID 4 | 5 | from ..utils.pydantic_version import PYDANTIC_VERSION 6 | 7 | if PYDANTIC_VERSION < (2, 0): 8 | from pydantic import BaseModel, Extra, Field 9 | else: 10 | from pydantic.v1 import BaseModel, Extra, Field # type: ignore 11 | 12 | from ..config import PermitConfig 13 | from ..utils.sync import SyncClass 14 | from .base import BasePermitApi 15 | 16 | 17 | class EmbeddedLoginRequestOutput(BaseModel): 18 | class Config: 19 | extra = Extra.allow 20 | 21 | error: Optional[str] = Field( 22 | None, 23 | description="If the login request failed, this field will contain the error message", 24 | title="Error", 25 | ) 26 | error_code: Optional[int] = Field( 27 | None, 28 | description="If the login request failed, this field will contain the error code", 29 | title="Error Code", 30 | ) 31 | token: Optional[str] = Field( 32 | None, 33 | description="The auth token that lets your users login into permit elements", 34 | title="Token", 35 | ) 36 | extra: Optional[str] = Field( 37 | None, 38 | description="Extra data that you can pass to the login request", 39 | title="Extra", 40 | ) 41 | redirect_url: str = Field( 42 | ..., 43 | description="The full URL to which the user should be redirected in order to complete the login process", 44 | title="Redirect Url", 45 | ) 46 | 47 | 48 | class LoginAsErrorMessages(str, Enum): 49 | USER_NOT_FOUND = "User not found" 50 | TENANT_NOT_FOUND = "Tenant not found" 51 | INVALID_PERMISSION_LEVEL = "Invalid user permission level" 52 | FORBIDDEN_ACCESS = "Forbidden access" 53 | 54 | 55 | class LoginAsSchema(BaseModel): 56 | """ 57 | Represents the schema for the loginAs request. 58 | """ 59 | 60 | user_id: str = Field(..., description="The key (or ID) of the user the element will log in as.") 61 | tenant_id: str = Field( 62 | ..., 63 | description="The key (or ID) of the active tenant for the logged in user." 64 | + "The embedded user will only be able to access the active tenant.", 65 | ) 66 | 67 | 68 | class UserLoginAsResponse(EmbeddedLoginRequestOutput): 69 | content: Optional[dict] = Field( 70 | None, 71 | description="Content to return in the response body for header/bearer login", 72 | ) 73 | 74 | 75 | class ElementsApi(BasePermitApi): 76 | def __init__(self, config: PermitConfig): 77 | super().__init__(config) 78 | self.__auth = self._build_http_client("/v2/auth") 79 | 80 | async def login_as(self, user_id: Union[str, UUID], tenant_id: Union[str, UUID]) -> UserLoginAsResponse: 81 | if isinstance(user_id, UUID): 82 | user_id = user_id.hex 83 | if isinstance(tenant_id, UUID): 84 | tenant_id = tenant_id.hex 85 | ticket = await self.__auth.post( 86 | "/elements_login_as", 87 | model=EmbeddedLoginRequestOutput, 88 | json=LoginAsSchema(user_id=user_id, tenant_id=tenant_id), 89 | ) 90 | return UserLoginAsResponse(**ticket.dict(), content={"url": ticket.redirect_url}) 91 | 92 | 93 | class SyncElementsApi(ElementsApi, metaclass=SyncClass): 94 | pass 95 | -------------------------------------------------------------------------------- /permit/api/projects.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from ..config import PermitConfig 11 | from .base import ( 12 | BasePermitApi, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ProjectCreate, ProjectRead, ProjectUpdate 17 | 18 | 19 | class ProjectsApi(BasePermitApi): 20 | def __init__(self, config: PermitConfig): 21 | super().__init__(config) 22 | self.__projects = self._build_http_client("/v2/projects") 23 | 24 | @validate_arguments # type: ignore[operator] 25 | async def list(self, page: int = 1, per_page: int = 100) -> List[ProjectRead]: 26 | """ 27 | Retrieves a list of projects. 28 | 29 | Args: 30 | page: The page number to fetch (default: 1). 31 | per_page: How many items to fetch per page (default: 100). 32 | 33 | Returns: 34 | A promise that resolves to an array of projects. 35 | 36 | Raises: 37 | PermitApiError: If the API returns an error HTTP status code. 38 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 39 | """ 40 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 41 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 42 | return await self.__projects.get("", model=List[ProjectRead], params=pagination_params(page, per_page)) 43 | 44 | async def _get(self, project_key: str) -> ProjectRead: 45 | return await self.__projects.get(f"/{project_key}", model=ProjectRead) 46 | 47 | @validate_arguments # type: ignore[operator] 48 | async def get(self, project_key: str) -> ProjectRead: 49 | """ 50 | Retrieves a project by its key. 51 | 52 | Args: 53 | project_key: The key of the project. 54 | 55 | Returns: 56 | A promise that resolves to the project. 57 | 58 | Raises: 59 | PermitApiError: If the API returns an error HTTP status code. 60 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 61 | """ 62 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 63 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 64 | return await self._get(project_key) 65 | 66 | @validate_arguments # type: ignore[operator] 67 | async def get_by_key(self, project_key: str) -> ProjectRead: 68 | """ 69 | Retrieves a project by its key. 70 | Alias for the get method. 71 | 72 | Args: 73 | project_key: The key of the project. 74 | 75 | Returns: 76 | A promise that resolves to the project. 77 | 78 | Raises: 79 | PermitApiError: If the API returns an error HTTP status code. 80 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 81 | """ 82 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 83 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 84 | return await self._get(project_key) 85 | 86 | @validate_arguments # type: ignore[operator] 87 | async def get_by_id(self, project_id: str) -> ProjectRead: 88 | """ 89 | Retrieves a project by its ID. 90 | Alias for the get method. 91 | 92 | Args: 93 | project_id: The ID of the project. 94 | 95 | Returns: 96 | A promise that resolves to the project. 97 | 98 | Raises: 99 | PermitApiError: If the API returns an error HTTP status code. 100 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 101 | """ 102 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 103 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 104 | return await self._get(project_id) 105 | 106 | @validate_arguments # type: ignore[operator] 107 | async def create(self, project_data: ProjectCreate) -> ProjectRead: 108 | """ 109 | Creates a new project. 110 | 111 | Args: 112 | project_data: The data for the new project. 113 | 114 | Returns: 115 | A promise that resolves to the created project. 116 | 117 | Raises: 118 | PermitApiError: If the API returns an error HTTP status code. 119 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 120 | """ 121 | await self._ensure_access_level(ApiKeyAccessLevel.ORGANIZATION_LEVEL_API_KEY) 122 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 123 | return await self.__projects.post("", model=ProjectRead, json=project_data) 124 | 125 | @validate_arguments # type: ignore[operator] 126 | async def update(self, project_key: str, project_data: ProjectUpdate) -> ProjectRead: 127 | """ 128 | Updates a project. 129 | 130 | Args: 131 | project_key: The key of the project. 132 | project_data: The updated data for the project. 133 | 134 | Returns: 135 | A promise that resolves to the updated project. 136 | 137 | Raises: 138 | PermitApiError: If the API returns an error HTTP status code. 139 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 140 | """ 141 | await self._ensure_access_level(ApiKeyAccessLevel.PROJECT_LEVEL_API_KEY) 142 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 143 | return await self.__projects.patch(f"/{project_key}", model=ProjectRead, json=project_data) 144 | 145 | @validate_arguments # type: ignore[operator] 146 | async def delete(self, project_key: str) -> None: 147 | """ 148 | Deletes a project. 149 | 150 | Args: 151 | project_key: The key of the project to delete. 152 | 153 | Returns: 154 | A promise that resolves when the project is deleted. 155 | 156 | Raises: 157 | PermitApiError: If the API returns an error HTTP status code. 158 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 159 | """ 160 | await self._ensure_access_level(ApiKeyAccessLevel.PROJECT_LEVEL_API_KEY) 161 | await self._ensure_context(ApiContextLevel.ORGANIZATION) 162 | return await self.__projects.delete(f"/{project_key}") 163 | -------------------------------------------------------------------------------- /permit/api/relationship_tuples.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ( 17 | RelationshipTupleCreate, 18 | RelationshipTupleCreateBulkOperation, 19 | RelationshipTupleCreateBulkOperationResult, 20 | RelationshipTupleDelete, 21 | RelationshipTupleDeleteBulkOperation, 22 | RelationshipTupleDeleteBulkOperationResult, 23 | RelationshipTupleRead, 24 | ) 25 | 26 | 27 | class RelationshipTuplesApi(BasePermitApi): 28 | @property 29 | def __relationship_tuples(self) -> SimpleHttpClient: 30 | if self.config.proxy_facts_via_pdp: 31 | return self._build_http_client("/facts/relationship_tuples", use_pdp=True) 32 | else: 33 | return self._build_http_client( 34 | f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/relationship_tuples" 35 | ) 36 | 37 | @validate_arguments # type: ignore[operator] 38 | async def list( 39 | self, 40 | page: int = 1, 41 | per_page: int = 100, 42 | subject_key: Optional[str] = None, 43 | relation_key: Optional[str] = None, 44 | object_key: Optional[str] = None, 45 | tenant_key: Optional[str] = None, 46 | ) -> List[RelationshipTupleRead]: 47 | """ 48 | Retrieves a list of relationship tuples based on the specified filters. 49 | 50 | Args: 51 | page: The page number to fetch (default: 1). 52 | per_page: How many items to fetch per page (default: 100). 53 | subject_key: if specified, only relationship tuples with this subject will be fetched. 54 | relation_key: if specified, only relationship tuples with this relation will be fetched. 55 | object_key: if specified, only relationship tuples with this object will be fetched. 56 | tenant_key: if specified, only relationship tuples with this tenant will be fetched. 57 | 58 | Returns: 59 | an array of relationship tuples. 60 | 61 | Raises: 62 | PermitApiError: If the API returns an error HTTP status code. 63 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 64 | """ 65 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 66 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 67 | params = list(pagination_params(page, per_page).items()) 68 | 69 | if subject_key is not None: 70 | params.append(("subject", subject_key)) 71 | if relation_key is not None: 72 | params.append(("relation", relation_key)) 73 | if object_key is not None: 74 | params.append(("object", object_key)) 75 | if tenant_key is not None: 76 | params.append(("tenant", tenant_key)) 77 | 78 | return await self.__relationship_tuples.get( 79 | "", 80 | model=List[RelationshipTupleRead], 81 | params=params, 82 | ) 83 | 84 | @validate_arguments # type: ignore[operator] 85 | async def create(self, tuple_data: RelationshipTupleCreate) -> RelationshipTupleRead: 86 | """ 87 | Creates a new relationship tuple, that states that a relationship (of type: relation) 88 | exists between two resource instances: the subject and the object. 89 | 90 | Args: 91 | tuple_data: The relationship tuple to create. 92 | 93 | Returns: 94 | the created relationship tuple. 95 | 96 | Raises: 97 | PermitApiError: If the API returns an error HTTP status code. 98 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 99 | """ 100 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 101 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 102 | return await self.__relationship_tuples.post("", model=RelationshipTupleRead, json=tuple_data) 103 | 104 | @validate_arguments # type: ignore[operator] 105 | async def delete(self, tuple_data: RelationshipTupleDelete) -> None: 106 | """ 107 | Removes a relationship tuple. 108 | 109 | Args: 110 | tuple_data: The relationship tuple to delete. 111 | 112 | Raises: 113 | PermitApiError: If the API returns an error HTTP status code. 114 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 115 | """ 116 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 117 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 118 | return await self.__relationship_tuples.delete("", json=tuple_data) 119 | 120 | @validate_arguments # type: ignore[operator] 121 | async def bulk_create(self, tuples: List[RelationshipTupleCreate]) -> RelationshipTupleCreateBulkOperationResult: 122 | """ 123 | Creates multiple relationship tuples at once using the provided tuple data. 124 | 125 | Args: 126 | tuples: The relationship tuples to create. 127 | Each tuple object is of type RelationshipTupleCreate and is essentially 128 | a tuple of (subject, relation, object, tenant). 129 | 130 | subject and object are both resource instances, formatted as 131 | `` strings (e.g: Folder:budget23). 132 | relation is the name of the relation. 133 | tenant is the key of the tenant in which to place the relation 134 | (optional if at least one of subject/object already exists). 135 | 136 | Subject and object must both be resource instances *in the same tenant*! 137 | 138 | Returns: 139 | the tuples creation result (RelationshipTupleCreateBulkOperationResult) 140 | 141 | Raises: 142 | PermitApiError: If the API returns an error HTTP status code. 143 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 144 | """ 145 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 146 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 147 | return await self.__relationship_tuples.post( 148 | "/bulk", 149 | model=RelationshipTupleCreateBulkOperationResult, 150 | json=RelationshipTupleCreateBulkOperation(operations=tuples), 151 | ) 152 | 153 | @validate_arguments # type: ignore[operator] 154 | async def bulk_delete(self, tuples: List[RelationshipTupleDelete]) -> RelationshipTupleDeleteBulkOperationResult: 155 | """ 156 | Deletes multiple relationship tuples at once using the provided tuple data. 157 | 158 | Args: 159 | tuples: The relationship tuples to delete. 160 | Each tuple object is of type RelationshipTupleDelete and is essentially 161 | a tuple of (subject, relation, object). 162 | 163 | subject and object are both resource instances, formatted as 164 | `` strings (e.g: Folder:budget23). 165 | relation is the name of the relation. 166 | 167 | Returns: 168 | the tuples deletion result (RelationshipTupleDeleteBulkOperationResult) 169 | 170 | Raises: 171 | PermitApiError: If the API returns an error HTTP status code. 172 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 173 | """ 174 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 175 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 176 | return await self.__relationship_tuples.delete( 177 | "/bulk", 178 | model=RelationshipTupleDeleteBulkOperationResult, 179 | json=RelationshipTupleDeleteBulkOperation(idents=tuples), 180 | ) 181 | -------------------------------------------------------------------------------- /permit/api/resource_action_groups.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ( 17 | ResourceActionGroupCreate, 18 | ResourceActionGroupRead, 19 | ResourceActionGroupUpdate, 20 | ) 21 | 22 | 23 | class ResourceActionGroupsApi(BasePermitApi): 24 | @property 25 | def __action_groups(self) -> SimpleHttpClient: 26 | return self._build_http_client( 27 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/resources" 28 | ) 29 | 30 | @validate_arguments # type: ignore[operator] 31 | async def list(self, resource_key: str, page: int = 1, per_page: int = 100) -> List[ResourceActionGroupRead]: 32 | """ 33 | Retrieves a list of action groups. 34 | 35 | Args: 36 | resource_key: The key of the resource to filter on. 37 | page: The page number to fetch (default: 1). 38 | per_page: How many items to fetch per page (default: 100). 39 | 40 | Returns: 41 | an array of action groups. 42 | 43 | Raises: 44 | PermitApiError: If the API returns an error HTTP status code. 45 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 46 | """ 47 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 48 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 49 | return await self.__action_groups.get( 50 | f"/{resource_key}/action_groups", 51 | model=List[ResourceActionGroupRead], 52 | params=pagination_params(page, per_page), 53 | ) 54 | 55 | async def _get(self, resource_key: str, group_key: str) -> ResourceActionGroupRead: 56 | return await self.__action_groups.get( 57 | f"/{resource_key}/action_groups/{group_key}", 58 | model=ResourceActionGroupRead, 59 | ) 60 | 61 | @validate_arguments # type: ignore[operator] 62 | async def get(self, resource_key: str, group_key: str) -> ResourceActionGroupRead: 63 | """ 64 | Retrieves a action group by its key. 65 | 66 | Args: 67 | resource_key: The key of the resource the action group belongs to. 68 | group_key: The key of the action group. 69 | 70 | Returns: 71 | the action group. 72 | 73 | Raises: 74 | PermitApiError: If the API returns an error HTTP status code. 75 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 76 | """ 77 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 78 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 79 | return await self._get(resource_key, group_key) 80 | 81 | @validate_arguments # type: ignore[operator] 82 | async def get_by_key(self, resource_key: str, group_key: str) -> ResourceActionGroupRead: 83 | """ 84 | Retrieves a action group by its key. 85 | Alias for the get method. 86 | 87 | Args: 88 | resource_key: The key of the resource the action group belongs to. 89 | group_key: The key of the action group. 90 | 91 | Returns: 92 | the action group. 93 | 94 | Raises: 95 | PermitApiError: If the API returns an error HTTP status code. 96 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 97 | """ 98 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 99 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 100 | return await self._get(resource_key, group_key) 101 | 102 | @validate_arguments # type: ignore[operator] 103 | async def get_by_id(self, resource_id: str, group_id: str) -> ResourceActionGroupRead: 104 | """ 105 | Retrieves a action group by its ID. 106 | Alias for the get method. 107 | 108 | Args: 109 | resource_key: The ID of the resource the action group belongs to. 110 | group_id: The ID of the action group. 111 | 112 | Returns: 113 | the action group. 114 | 115 | Raises: 116 | PermitApiError: If the API returns an error HTTP status code. 117 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 118 | """ 119 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 120 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 121 | return await self._get(resource_id, group_id) 122 | 123 | @validate_arguments # type: ignore[operator] 124 | async def create(self, resource_key: str, group_data: ResourceActionGroupCreate) -> ResourceActionGroupRead: 125 | """ 126 | Creates a new action group. 127 | 128 | Args: 129 | resource_key: The key of the resource under which the action group should be created. 130 | group_data: The data for the new action group. 131 | 132 | Returns: 133 | the created action group. 134 | 135 | Raises: 136 | PermitApiError: If the API returns an error HTTP status code. 137 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 138 | """ 139 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 140 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 141 | return await self.__action_groups.post( 142 | f"/{resource_key}/action_groups", 143 | model=ResourceActionGroupRead, 144 | json=group_data, 145 | ) 146 | 147 | @validate_arguments # type: ignore[operator] 148 | async def update( 149 | self, resource_key: str, group_key: str, group_data: ResourceActionGroupUpdate 150 | ) -> ResourceActionGroupRead: 151 | """ 152 | Updates an action group. 153 | 154 | Args: 155 | resource_key: The key of the resource the action group belongs to. 156 | group_key: The key of the action group. 157 | group_data: The updated data for the action group. 158 | 159 | Returns: 160 | the updated action group. 161 | 162 | Raises: 163 | PermitApiError: If the API returns an error HTTP status code. 164 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 165 | """ 166 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 167 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 168 | return await self.__action_groups.patch( 169 | f"/{resource_key}/action_groups/{group_key}", 170 | model=ResourceActionGroupRead, 171 | json=group_data, 172 | ) 173 | 174 | @validate_arguments # type: ignore[operator] 175 | async def delete(self, resource_key: str, group_key: str) -> None: 176 | """ 177 | Deletes a action group. 178 | 179 | Args: 180 | resource_key: The key of the resource the action group belongs to. 181 | group_key: The key of the action group to delete. 182 | 183 | Raises: 184 | PermitApiError: If the API returns an error HTTP status code. 185 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 186 | """ 187 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 188 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 189 | return await self.__action_groups.delete(f"/{resource_key}/action_groups/{group_key}") 190 | -------------------------------------------------------------------------------- /permit/api/resource_actions.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ResourceActionCreate, ResourceActionRead, ResourceActionUpdate 17 | 18 | 19 | class ResourceActionsApi(BasePermitApi): 20 | @property 21 | def __actions(self) -> SimpleHttpClient: 22 | return self._build_http_client( 23 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/resources" 24 | ) 25 | 26 | @validate_arguments # type: ignore[operator] 27 | async def list(self, resource_key: str, page: int = 1, per_page: int = 100) -> List[ResourceActionRead]: 28 | """ 29 | Retrieves a list of actions. 30 | 31 | Args: 32 | resource_key: The key of the resource to filter on. 33 | page: The page number to fetch (default: 1). 34 | per_page: How many items to fetch per page (default: 100). 35 | 36 | Returns: 37 | an array of actions. 38 | 39 | Raises: 40 | PermitApiError: If the API returns an error HTTP status code. 41 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 42 | """ 43 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 44 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 45 | return await self.__actions.get( 46 | f"/{resource_key}/actions", 47 | model=List[ResourceActionRead], 48 | params=pagination_params(page, per_page), 49 | ) 50 | 51 | async def _get(self, resource_key: str, action_key: str) -> ResourceActionRead: 52 | return await self.__actions.get(f"/{resource_key}/actions/{action_key}", model=ResourceActionRead) 53 | 54 | @validate_arguments # type: ignore[operator] 55 | async def get(self, resource_key: str, action_key: str) -> ResourceActionRead: 56 | """ 57 | Retrieves a action by its key. 58 | 59 | Args: 60 | resource_key: The key of the resource the action belongs to. 61 | action_key: The key of the action. 62 | 63 | Returns: 64 | the action. 65 | 66 | Raises: 67 | PermitApiError: If the API returns an error HTTP status code. 68 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 69 | """ 70 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 71 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 72 | return await self._get(resource_key, action_key) 73 | 74 | @validate_arguments # type: ignore[operator] 75 | async def get_by_key(self, resource_key: str, action_key: str) -> ResourceActionRead: 76 | """ 77 | Retrieves a action by its key. 78 | Alias for the get method. 79 | 80 | Args: 81 | resource_key: The key of the resource the action belongs to. 82 | action_key: The key of the action. 83 | 84 | Returns: 85 | the action. 86 | 87 | Raises: 88 | PermitApiError: If the API returns an error HTTP status code. 89 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 90 | """ 91 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 92 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 93 | return await self._get(resource_key, action_key) 94 | 95 | @validate_arguments # type: ignore[operator] 96 | async def get_by_id(self, resource_id: str, action_id: str) -> ResourceActionRead: 97 | """ 98 | Retrieves a action by its ID. 99 | Alias for the get method. 100 | 101 | Args: 102 | resource_key: The ID of the resource the action belongs to. 103 | action_id: The ID of the action. 104 | 105 | Returns: 106 | the action. 107 | 108 | Raises: 109 | PermitApiError: If the API returns an error HTTP status code. 110 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 111 | """ 112 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 113 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 114 | return await self._get(resource_id, action_id) 115 | 116 | @validate_arguments # type: ignore[operator] 117 | async def create(self, resource_key: str, action_data: ResourceActionCreate) -> ResourceActionRead: 118 | """ 119 | Creates a new action. 120 | 121 | Args: 122 | resource_key: The key of the resource under which the action should be created. 123 | action_data: The data for the new action. 124 | 125 | Returns: 126 | the created action. 127 | 128 | Raises: 129 | PermitApiError: If the API returns an error HTTP status code. 130 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 131 | """ 132 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 133 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 134 | return await self.__actions.post( 135 | f"/{resource_key}/actions", 136 | model=ResourceActionRead, 137 | json=action_data, 138 | ) 139 | 140 | @validate_arguments # type: ignore[operator] 141 | async def update(self, resource_key: str, action_key: str, action_data: ResourceActionUpdate) -> ResourceActionRead: 142 | """ 143 | Updates a action. 144 | 145 | Args: 146 | resource_key: The key of the resource the action belongs to. 147 | action_key: The key of the action. 148 | action_data: The updated data for the action. 149 | 150 | Returns: 151 | the updated action. 152 | 153 | Raises: 154 | PermitApiError: If the API returns an error HTTP status code. 155 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 156 | """ 157 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 158 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 159 | return await self.__actions.patch( 160 | f"/{resource_key}/actions/{action_key}", 161 | model=ResourceActionRead, 162 | json=action_data, 163 | ) 164 | 165 | @validate_arguments # type: ignore[operator] 166 | async def delete(self, resource_key: str, action_key: str) -> None: 167 | """ 168 | Deletes a action. 169 | 170 | Args: 171 | resource_key: The key of the resource the action belongs to. 172 | action_key: The key of the action to delete. 173 | 174 | Raises: 175 | PermitApiError: If the API returns an error HTTP status code. 176 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 177 | """ 178 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 179 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 180 | return await self.__actions.delete(f"/{resource_key}/actions/{action_key}") 181 | -------------------------------------------------------------------------------- /permit/api/resource_attributes.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ( 17 | ResourceAttributeCreate, 18 | ResourceAttributeRead, 19 | ResourceAttributeUpdate, 20 | ) 21 | 22 | 23 | class ResourceAttributesApi(BasePermitApi): 24 | @property 25 | def __attributes(self) -> SimpleHttpClient: 26 | return self._build_http_client( 27 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/resources" 28 | ) 29 | 30 | @validate_arguments # type: ignore[operator] 31 | async def list(self, resource_key: str, page: int = 1, per_page: int = 100) -> List[ResourceAttributeRead]: 32 | """ 33 | Retrieves a list of attributes. 34 | 35 | Args: 36 | resource_key: The key of the resource to filter on. 37 | page: The page number to fetch (default: 1). 38 | per_page: How many items to fetch per page (default: 100). 39 | 40 | Returns: 41 | an array of attributes. 42 | 43 | Raises: 44 | PermitApiError: If the API returns an error HTTP status code. 45 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 46 | """ 47 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 48 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 49 | return await self.__attributes.get( 50 | f"/{resource_key}/attributes", 51 | model=List[ResourceAttributeRead], 52 | params=pagination_params(page, per_page), 53 | ) 54 | 55 | async def _get(self, resource_key: str, attribute_key: str) -> ResourceAttributeRead: 56 | return await self.__attributes.get(f"/{resource_key}/attributes/{attribute_key}", model=ResourceAttributeRead) 57 | 58 | @validate_arguments # type: ignore[operator] 59 | async def get(self, resource_key: str, attribute_key: str) -> ResourceAttributeRead: 60 | """ 61 | Retrieves a attribute by its key. 62 | 63 | Args: 64 | resource_key: The key of the resource the attribute belongs to. 65 | attribute_key: The key of the attribute. 66 | 67 | Returns: 68 | the attribute. 69 | 70 | Raises: 71 | PermitApiError: If the API returns an error HTTP status code. 72 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 73 | """ 74 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 75 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 76 | return await self._get(resource_key, attribute_key) 77 | 78 | @validate_arguments # type: ignore[operator] 79 | async def get_by_key(self, resource_key: str, attribute_key: str) -> ResourceAttributeRead: 80 | """ 81 | Retrieves a attribute by its key. 82 | Alias for the get method. 83 | 84 | Args: 85 | resource_key: The key of the resource the attribute belongs to. 86 | attribute_key: The key of the attribute. 87 | 88 | Returns: 89 | the attribute. 90 | 91 | Raises: 92 | PermitApiError: If the API returns an error HTTP status code. 93 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 94 | """ 95 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 96 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 97 | return await self._get(resource_key, attribute_key) 98 | 99 | @validate_arguments # type: ignore[operator] 100 | async def get_by_id(self, resource_id: str, attribute_id: str) -> ResourceAttributeRead: 101 | """ 102 | Retrieves a attribute by its ID. 103 | Alias for the get method. 104 | 105 | Args: 106 | resource_key: The ID of the resource the attribute belongs to. 107 | attribute_id: The ID of the attribute. 108 | 109 | Returns: 110 | the attribute. 111 | 112 | Raises: 113 | PermitApiError: If the API returns an error HTTP status code. 114 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 115 | """ 116 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 117 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 118 | return await self._get(resource_id, attribute_id) 119 | 120 | @validate_arguments # type: ignore[operator] 121 | async def create(self, resource_key: str, attribute_data: ResourceAttributeCreate) -> ResourceAttributeRead: 122 | """ 123 | Creates a new attribute. 124 | 125 | Args: 126 | resource_key: The key of the resource under which the attribute should be created. 127 | attribute_data: The data for the new attribute. 128 | 129 | Returns: 130 | the created attribute. 131 | 132 | Raises: 133 | PermitApiError: If the API returns an error HTTP status code. 134 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 135 | """ 136 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 137 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 138 | return await self.__attributes.post( 139 | f"/{resource_key}/attributes", 140 | model=ResourceAttributeRead, 141 | json=attribute_data, 142 | ) 143 | 144 | @validate_arguments # type: ignore[operator] 145 | async def update( 146 | self, 147 | resource_key: str, 148 | attribute_key: str, 149 | attribute_data: ResourceAttributeUpdate, 150 | ) -> ResourceAttributeRead: 151 | """ 152 | Updates a attribute. 153 | 154 | Args: 155 | resource_key: The key of the resource the attribute belongs to. 156 | attribute_key: The key of the attribute. 157 | attribute_data: The updated data for the attribute. 158 | 159 | Returns: 160 | the updated attribute. 161 | 162 | Raises: 163 | PermitApiError: If the API returns an error HTTP status code. 164 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 165 | """ 166 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 167 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 168 | return await self.__attributes.patch( 169 | f"/{resource_key}/attributes/{attribute_key}", 170 | model=ResourceAttributeRead, 171 | json=attribute_data, 172 | ) 173 | 174 | @validate_arguments # type: ignore[operator] 175 | async def delete(self, resource_key: str, attribute_key: str) -> None: 176 | """ 177 | Deletes a attribute. 178 | 179 | Args: 180 | resource_key: The key of the resource the attribute belongs to. 181 | attribute_key: The key of the attribute to delete. 182 | 183 | Raises: 184 | PermitApiError: If the API returns an error HTTP status code. 185 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 186 | """ 187 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 188 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 189 | return await self.__attributes.delete(f"/{resource_key}/attributes/{attribute_key}") 190 | -------------------------------------------------------------------------------- /permit/api/resource_relations.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import RelationCreate, RelationRead 17 | 18 | 19 | class ResourceRelationsApi(BasePermitApi): 20 | @property 21 | def __relations(self) -> SimpleHttpClient: 22 | return self._build_http_client( 23 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/resources" 24 | ) 25 | 26 | @validate_arguments # type: ignore[operator] 27 | async def list(self, resource_key: str, page: int = 1, per_page: int = 100) -> List[RelationRead]: 28 | """ 29 | Retrieves a list of outgoing relations originating in a specific (object) resource. 30 | 31 | Args: 32 | resource_key: The key of the resource to filter on. 33 | page: The page number to fetch (default: 1). 34 | per_page: How many items to fetch per page (default: 100). 35 | 36 | Returns: 37 | an array of relations. 38 | 39 | Raises: 40 | PermitApiError: If the API returns an error HTTP status code. 41 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 42 | """ 43 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 44 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 45 | return await self.__relations.get( 46 | f"/{resource_key}/relations", 47 | model=List[RelationRead], 48 | params=pagination_params(page, per_page), 49 | ) 50 | 51 | async def _get(self, resource_key: str, relation_key: str) -> RelationRead: 52 | return await self.__relations.get(f"/{resource_key}/relations/{relation_key}", model=RelationRead) 53 | 54 | @validate_arguments # type: ignore[operator] 55 | async def get(self, resource_key: str, relation_key: str) -> RelationRead: 56 | """ 57 | Retrieves a relation by its key. 58 | 59 | Args: 60 | resource_key: The key of the resource the relation belongs to. 61 | relation_key: The key of the relation. 62 | 63 | Returns: 64 | the relation. 65 | 66 | Raises: 67 | PermitApiError: If the API returns an error HTTP status code. 68 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 69 | """ 70 | 71 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 72 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 73 | return await self._get(resource_key, relation_key) 74 | 75 | @validate_arguments # type: ignore[operator] 76 | async def get_by_key(self, resource_key: str, relation_key: str) -> RelationRead: 77 | """ 78 | Retrieves a relation by its key. 79 | Alias for the get method. 80 | 81 | Args: 82 | resource_key: The key of the resource the relation belongs to. 83 | relation_key: The key of the relation. 84 | 85 | Returns: 86 | the relation. 87 | 88 | Raises: 89 | PermitApiError: If the API returns an error HTTP status code. 90 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 91 | """ 92 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 93 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 94 | return await self._get(resource_key, relation_key) 95 | 96 | @validate_arguments # type: ignore[operator] 97 | async def get_by_id(self, resource_id: str, relation_id: str) -> RelationRead: 98 | """ 99 | Retrieves a relation by its ID. 100 | Alias for the get method. 101 | 102 | Args: 103 | resource_key: The ID of the resource the relation belongs to. 104 | relation_id: The ID of the relation. 105 | 106 | Returns: 107 | the relation. 108 | 109 | Raises: 110 | PermitApiError: If the API returns an error HTTP status code. 111 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 112 | """ 113 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 114 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 115 | return await self._get(resource_id, relation_id) 116 | 117 | @validate_arguments # type: ignore[operator] 118 | async def create(self, resource_key: str, relation_data: RelationCreate) -> RelationRead: 119 | """ 120 | Creates a new relation. 121 | 122 | Args: 123 | resource_key: The key of the resource under which the relation should be created. 124 | relation_data: The data for the new relation. 125 | 126 | Returns: 127 | the created relation. 128 | 129 | Raises: 130 | PermitApiError: If the API returns an error HTTP status code. 131 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 132 | """ 133 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 134 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 135 | return await self.__relations.post( 136 | f"/{resource_key}/relations", 137 | model=RelationRead, 138 | json=relation_data, 139 | ) 140 | 141 | @validate_arguments # type: ignore[operator] 142 | async def delete(self, resource_key: str, relation_key: str) -> None: 143 | """ 144 | Deletes a relation. 145 | 146 | Args: 147 | resource_key: The key of the resource the relation belongs to. 148 | relation_key: The key of the relation to delete. 149 | 150 | Raises: 151 | PermitApiError: If the API returns an error HTTP status code. 152 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 153 | """ 154 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 155 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 156 | return await self.__relations.delete(f"/{resource_key}/relations/{relation_key}") 157 | -------------------------------------------------------------------------------- /permit/api/resources.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ResourceCreate, ResourceRead, ResourceReplace, ResourceUpdate 17 | 18 | 19 | class ResourcesApi(BasePermitApi): 20 | @property 21 | def __resources(self) -> SimpleHttpClient: 22 | return self._build_http_client( 23 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/resources" 24 | ) 25 | 26 | @validate_arguments # type: ignore[operator] 27 | async def list(self, page: int = 1, per_page: int = 100) -> List[ResourceRead]: 28 | """ 29 | Retrieves a list of resources. 30 | 31 | Args: 32 | page: The page number to fetch (default: 1). 33 | per_page: How many items to fetch per page (default: 100). 34 | 35 | Returns: 36 | an array of resources. 37 | 38 | Raises: 39 | PermitApiError: If the API returns an error HTTP status code. 40 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 41 | """ 42 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 43 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 44 | return await self.__resources.get( 45 | "", 46 | model=List[ResourceRead], 47 | params=pagination_params(page, per_page), 48 | ) 49 | 50 | async def _get(self, resource_key: str) -> ResourceRead: 51 | return await self.__resources.get(f"/{resource_key}", model=ResourceRead) 52 | 53 | @validate_arguments # type: ignore[operator] 54 | async def get(self, resource_key: str) -> ResourceRead: 55 | """ 56 | Retrieves a resource by its key. 57 | 58 | Args: 59 | resource_key: The key of the resource. 60 | 61 | Returns: 62 | the resource. 63 | 64 | Raises: 65 | PermitApiError: If the API returns an error HTTP status code. 66 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 67 | """ 68 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 69 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 70 | return await self._get(resource_key) 71 | 72 | @validate_arguments # type: ignore[operator] 73 | async def get_by_key(self, resource_key: str) -> ResourceRead: 74 | """ 75 | Retrieves a resource by its key. 76 | Alias for the get method. 77 | 78 | Args: 79 | resource_key: The key of the resource. 80 | 81 | Returns: 82 | the resource. 83 | 84 | Raises: 85 | PermitApiError: If the API returns an error HTTP status code. 86 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 87 | """ 88 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 89 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 90 | return await self._get(resource_key) 91 | 92 | @validate_arguments # type: ignore[operator] 93 | async def get_by_id(self, resource_id: str) -> ResourceRead: 94 | """ 95 | Retrieves a resource by its ID. 96 | Alias for the get method. 97 | 98 | Args: 99 | resource_id: The ID of the resource. 100 | 101 | Returns: 102 | the resource. 103 | 104 | Raises: 105 | PermitApiError: If the API returns an error HTTP status code. 106 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 107 | """ 108 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 109 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 110 | return await self._get(resource_id) 111 | 112 | @validate_arguments # type: ignore[operator] 113 | async def create(self, resource_data: ResourceCreate) -> ResourceRead: 114 | """ 115 | Creates a new resource. 116 | 117 | Args: 118 | resource_data: The data for the new resource. 119 | 120 | Returns: 121 | the created resource. 122 | 123 | Raises: 124 | PermitApiError: If the API returns an error HTTP status code. 125 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 126 | """ 127 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 128 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 129 | return await self.__resources.post("", model=ResourceRead, json=resource_data) 130 | 131 | @validate_arguments # type: ignore[operator] 132 | async def update(self, resource_key: str, resource_data: ResourceUpdate) -> ResourceRead: 133 | """ 134 | Updates a resource. 135 | 136 | Args: 137 | resource_key: The key of the resource. 138 | resource_data: The updated data for the resource. 139 | 140 | Returns: 141 | the updated resource. 142 | 143 | Raises: 144 | PermitApiError: If the API returns an error HTTP status code. 145 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 146 | """ 147 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 148 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 149 | return await self.__resources.patch( 150 | f"/{resource_key}", 151 | model=ResourceRead, 152 | json=resource_data, 153 | ) 154 | 155 | @validate_arguments # type: ignore[operator] 156 | async def replace(self, resource_key: str, resource_data: ResourceReplace) -> ResourceRead: 157 | """ 158 | Creates a resource if no such resource exists, otherwise completely replaces the resource in place. 159 | 160 | Args: 161 | resource_key: The key of the resource. 162 | resource_data: The updated data for the resource. 163 | 164 | Returns: 165 | the updated resource. 166 | 167 | Raises: 168 | PermitApiError: If the API returns an error HTTP status code. 169 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 170 | """ 171 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 172 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 173 | return await self.__resources.put( 174 | f"/{resource_key}", 175 | model=ResourceRead, 176 | json=resource_data, 177 | ) 178 | 179 | @validate_arguments # type: ignore[operator] 180 | async def delete(self, resource_key: str) -> None: 181 | """ 182 | Deletes a resource. 183 | 184 | Args: 185 | resource_key: The key of the resource to delete. 186 | 187 | Raises: 188 | PermitApiError: If the API returns an error HTTP status code. 189 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 190 | """ 191 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 192 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 193 | return await self.__resources.delete(f"/{resource_key}") 194 | -------------------------------------------------------------------------------- /permit/api/role_assignments.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ( 17 | BulkRoleAssignmentReport, 18 | BulkRoleUnAssignmentReport, 19 | RoleAssignmentCreate, 20 | RoleAssignmentRead, 21 | RoleAssignmentRemove, 22 | ) 23 | 24 | 25 | class RoleAssignmentsApi(BasePermitApi): 26 | @property 27 | def __role_assignments(self) -> SimpleHttpClient: 28 | if self.config.proxy_facts_via_pdp: 29 | return self._build_http_client("/facts/role_assignments", use_pdp=True) 30 | else: 31 | return self._build_http_client( 32 | f"/v2/facts/{self.config.api_context.project}/{self.config.api_context.environment}/role_assignments" 33 | ) 34 | 35 | @validate_arguments # type: ignore[operator] 36 | async def list( 37 | self, 38 | user_key: Optional[Union[str, List[str]]] = None, 39 | role_key: Optional[Union[str, List[str]]] = None, 40 | tenant_key: Optional[Union[str, List[str]]] = None, 41 | resource_key: Optional[str] = None, 42 | resource_instance_key: Optional[str] = None, 43 | page: int = 1, 44 | per_page: int = 100, 45 | ) -> List[RoleAssignmentRead]: 46 | """ 47 | Retrieves a list of role assignments based on the specified filters. 48 | 49 | Args: 50 | user_key: if specified, only role granted to this user will be fetched. 51 | role_key: if specified, only assignments of this role will be fetched. 52 | tenant_key: (for roles) if specified, only role granted within this tenant will be fetched. 53 | resource_key: (for resource roles) if specified, only roles granted on instances of this resource type will be fetched. 54 | resource_instance_key: (for resource roles) if specified, only roles granted with this instance as the object will be fetched. 55 | page: The page number to fetch (default: 1). 56 | per_page: How many items to fetch per page (default: 100). 57 | 58 | Returns: 59 | an array of role assignments. 60 | 61 | Raises: 62 | PermitApiError: If the API returns an error HTTP status code. 63 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 64 | """ # noqa: E501 65 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 66 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 67 | params = list(pagination_params(page, per_page).items()) 68 | if user_key is not None: 69 | if isinstance(user_key, list): 70 | for user in user_key: 71 | params.append(("user", user)) 72 | else: 73 | params.append(("user", user_key)) 74 | if role_key is not None: 75 | if isinstance(role_key, list): 76 | for role in role_key: 77 | params.append(("role", role)) 78 | else: 79 | params.append(("role", role_key)) 80 | if tenant_key is not None: 81 | if isinstance(tenant_key, list): 82 | for tenant in tenant_key: 83 | params.append(("tenant", tenant)) 84 | else: 85 | params.append(("tenant", tenant_key)) 86 | if resource_key is not None: 87 | params.append(("resource", resource_key)) 88 | if resource_instance_key is not None: 89 | params.append(("resource_instance", resource_instance_key)) 90 | return await self.__role_assignments.get( 91 | "", 92 | model=List[RoleAssignmentRead], 93 | params=params, 94 | ) 95 | 96 | @validate_arguments # type: ignore[operator] 97 | async def assign(self, assignment: RoleAssignmentCreate) -> RoleAssignmentRead: 98 | """ 99 | Assigns a role to a user in the scope of a given tenant. 100 | 101 | Args: 102 | assignment: The role assignment details. 103 | 104 | Returns: 105 | the assigned role. 106 | 107 | Raises: 108 | PermitApiError: If the API returns an error HTTP status code. 109 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 110 | """ 111 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 112 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 113 | return await self.__role_assignments.post("", model=RoleAssignmentRead, json=assignment) 114 | 115 | @validate_arguments # type: ignore[operator] 116 | async def unassign(self, unassignment: RoleAssignmentRemove) -> None: 117 | """ 118 | Unassigns a role from a user in the scope of a given tenant. 119 | 120 | Args: 121 | unassignment: The role unassignment details. 122 | 123 | Raises: 124 | PermitApiError: If the API returns an error HTTP status code. 125 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 126 | """ 127 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 128 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 129 | return await self.__role_assignments.delete("", json=unassignment) 130 | 131 | @validate_arguments # type: ignore[operator] 132 | async def bulk_assign(self, assignments: List[RoleAssignmentCreate]) -> BulkRoleAssignmentReport: 133 | """ 134 | Assigns multiple roles in bulk using the provided role assignments data. 135 | Each role assignment is a tuple of (user, role, tenant). 136 | 137 | Args: 138 | assignments: The role assignments to be performed in bulk. 139 | 140 | Returns: 141 | the bulk assignment report. 142 | 143 | Raises: 144 | PermitApiError: If the API returns an error HTTP status code. 145 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 146 | """ 147 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 148 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 149 | return await self.__role_assignments.post( 150 | "/bulk", 151 | model=BulkRoleAssignmentReport, 152 | json=list(assignments), 153 | ) 154 | 155 | @validate_arguments # type: ignore[operator] 156 | async def bulk_unassign(self, unassignments: List[RoleAssignmentRemove]) -> BulkRoleUnAssignmentReport: 157 | """ 158 | Removes multiple role assignments in bulk using the provided unassignment data. 159 | Each role to unassign is a tuple of (user, role, tenant). 160 | 161 | Args: 162 | unassignments: The role unassignments to be performed in bulk. 163 | 164 | Returns: 165 | the bulk unassignment report. 166 | 167 | Raises: 168 | PermitApiError: If the API returns an error HTTP status code. 169 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 170 | """ 171 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 172 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 173 | return await self.__role_assignments.delete( 174 | "/bulk", 175 | model=BulkRoleUnAssignmentReport, 176 | json=list(unassignments), 177 | ) 178 | -------------------------------------------------------------------------------- /permit/api/roles.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import validate_arguments 7 | else: 8 | from pydantic.v1 import validate_arguments 9 | 10 | from .base import ( 11 | BasePermitApi, 12 | SimpleHttpClient, 13 | pagination_params, 14 | ) 15 | from .context import ApiContextLevel, ApiKeyAccessLevel 16 | from .models import ( 17 | AddRolePermissions, 18 | RemoveRolePermissions, 19 | RoleCreate, 20 | RoleRead, 21 | RoleUpdate, 22 | ) 23 | 24 | 25 | class RolesApi(BasePermitApi): 26 | """ 27 | Represents the interface for managing roles. 28 | """ 29 | 30 | @property 31 | def __roles(self) -> SimpleHttpClient: 32 | return self._build_http_client( 33 | f"/v2/schema/{self.config.api_context.project}/{self.config.api_context.environment}/roles" 34 | ) 35 | 36 | @validate_arguments # type: ignore[operator] 37 | async def list(self, page: int = 1, per_page: int = 100) -> List[RoleRead]: 38 | """ 39 | Retrieves a list of roles. 40 | 41 | Args: 42 | page: The page number to fetch (default: 1). 43 | per_page: How many items to fetch per page (default: 100). 44 | 45 | Returns: 46 | A list of roles. 47 | 48 | Raises: 49 | PermitApiError: If the API returns an error HTTP status code. 50 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 51 | """ 52 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 53 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 54 | return await self.__roles.get("", model=List[RoleRead], params=pagination_params(page, per_page)) 55 | 56 | async def _get(self, role_key: str) -> RoleRead: 57 | return await self.__roles.get(f"/{role_key}", model=RoleRead) 58 | 59 | @validate_arguments # type: ignore[operator] 60 | async def get(self, role_key: str) -> RoleRead: 61 | """ 62 | Retrieves a role by its key. 63 | 64 | Args: 65 | role_key: The key of the role. 66 | 67 | Returns: 68 | The role. 69 | 70 | Raises: 71 | PermitApiError: If the API returns an error HTTP status code. 72 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 73 | """ 74 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 75 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 76 | return await self._get(role_key) 77 | 78 | @validate_arguments # type: ignore[operator] 79 | async def get_by_key(self, role_key: str) -> RoleRead: 80 | """ 81 | Retrieves a role by its key. 82 | Alias for the get method. 83 | 84 | Args: 85 | role_key: The key of the role. 86 | 87 | Returns: 88 | The role. 89 | 90 | Raises: 91 | PermitApiError: If the API returns an error HTTP status code. 92 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 93 | """ 94 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 95 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 96 | return await self._get(role_key) 97 | 98 | @validate_arguments # type: ignore[operator] 99 | async def get_by_id(self, role_id: str) -> RoleRead: 100 | """ 101 | Retrieves a role by its ID. 102 | Alias for the get method. 103 | 104 | Args: 105 | role_id: The ID of the role. 106 | 107 | Returns: 108 | The role. 109 | 110 | Raises: 111 | PermitApiError: If the API returns an error HTTP status code. 112 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 113 | """ 114 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 115 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 116 | return await self._get(role_id) 117 | 118 | @validate_arguments # type: ignore[operator] 119 | async def create(self, role_data: RoleCreate) -> RoleRead: 120 | """ 121 | Creates a new role. 122 | 123 | Args: 124 | role_data: The data for the new role. 125 | 126 | Returns: 127 | The created role. 128 | 129 | Raises: 130 | PermitApiError: If the API returns an error HTTP status code. 131 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 132 | """ 133 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 134 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 135 | return await self.__roles.post("", model=RoleRead, json=role_data) 136 | 137 | @validate_arguments # type: ignore[operator] 138 | async def update(self, role_key: str, role_data: RoleUpdate) -> RoleRead: 139 | """ 140 | Updates a role. 141 | 142 | Args: 143 | role_key: The key of the role. 144 | role_data: The updated data for the role. 145 | 146 | Returns: 147 | The updated role. 148 | 149 | Raises: 150 | PermitApiError: If the API returns an error HTTP status code. 151 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 152 | """ 153 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 154 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 155 | return await self.__roles.patch(f"/{role_key}", model=RoleRead, json=role_data) 156 | 157 | @validate_arguments # type: ignore[operator] 158 | async def delete(self, role_key: str) -> None: 159 | """ 160 | Deletes a role. 161 | 162 | Args: 163 | role_key: The key of the role to delete. 164 | 165 | Raises: 166 | PermitApiError: If the API returns an error HTTP status code. 167 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 168 | """ 169 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 170 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 171 | return await self.__roles.delete(f"/{role_key}") 172 | 173 | @validate_arguments # type: ignore[operator] 174 | async def assign_permissions(self, role_key: str, permissions: List[str]) -> RoleRead: 175 | """ 176 | Assigns permissions to a role. 177 | 178 | Args: 179 | role_key: The key of the role. 180 | permissions: An array of permission keys () to be assigned to the role. 181 | 182 | Returns: 183 | A RoleRead object representing the updated role. 184 | 185 | Raises: 186 | PermitApiError: If the API returns an error HTTP status code. 187 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 188 | """ 189 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 190 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 191 | return await self.__roles.post( 192 | f"/{role_key}/permissions", 193 | model=RoleRead, 194 | json=AddRolePermissions(permissions=permissions), 195 | ) 196 | 197 | @validate_arguments # type: ignore[operator] 198 | async def remove_permissions(self, role_key: str, permissions: List[str]) -> RoleRead: 199 | """ 200 | Removes permissions from a role. 201 | 202 | Args: 203 | role_key: The key of the role. 204 | permissions: An array of permission keys () to be removed from the role. 205 | 206 | Returns: 207 | A RoleRead object representing the updated role. 208 | 209 | Raises: 210 | PermitApiError: If the API returns an error HTTP status code. 211 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 212 | """ 213 | await self._ensure_access_level(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY) 214 | await self._ensure_context(ApiContextLevel.ENVIRONMENT) 215 | return await self.__roles.delete( 216 | f"/{role_key}/permissions", 217 | model=RoleRead, 218 | json=RemoveRolePermissions(permissions=permissions), 219 | ) 220 | -------------------------------------------------------------------------------- /permit/api/sync_api_client.py: -------------------------------------------------------------------------------- 1 | from ..config import PermitConfig 2 | from ..utils.sync import SyncClass 3 | from .condition_set_rules import ConditionSetRulesApi 4 | from .condition_sets import ConditionSetsApi 5 | from .deprecated import DeprecatedApi 6 | from .environments import EnvironmentsApi 7 | from .projects import ProjectsApi 8 | from .relationship_tuples import RelationshipTuplesApi 9 | from .resource_action_groups import ResourceActionGroupsApi 10 | from .resource_actions import ResourceActionsApi 11 | from .resource_attributes import ResourceAttributesApi 12 | from .resource_instances import ResourceInstancesApi 13 | from .resource_relations import ResourceRelationsApi 14 | from .resource_roles import ResourceRolesApi 15 | from .resources import ResourcesApi 16 | from .role_assignments import RoleAssignmentsApi 17 | from .roles import RolesApi 18 | from .tenants import TenantsApi 19 | from .users import UsersApi 20 | 21 | 22 | class SyncConditionSetRulesApi(ConditionSetRulesApi, metaclass=SyncClass): 23 | pass 24 | 25 | 26 | class SyncConditionSetsApi(ConditionSetsApi, metaclass=SyncClass): 27 | pass 28 | 29 | 30 | class SyncDeprecatedApi(DeprecatedApi, metaclass=SyncClass): 31 | pass 32 | 33 | 34 | class SyncEnvironmentsApi(EnvironmentsApi, metaclass=SyncClass): 35 | pass 36 | 37 | 38 | class SyncProjectsApi(ProjectsApi, metaclass=SyncClass): 39 | pass 40 | 41 | 42 | class SyncRelationshipTuplesApi(RelationshipTuplesApi, metaclass=SyncClass): 43 | pass 44 | 45 | 46 | class SyncResourceActionGroupsApi(ResourceActionGroupsApi, metaclass=SyncClass): 47 | pass 48 | 49 | 50 | class SyncResourceActionsApi(ResourceActionsApi, metaclass=SyncClass): 51 | pass 52 | 53 | 54 | class SyncResourceAttributesApi(ResourceAttributesApi, metaclass=SyncClass): 55 | pass 56 | 57 | 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 | 70 | class SyncResourcesApi(ResourcesApi, metaclass=SyncClass): 71 | pass 72 | 73 | 74 | class SyncRoleAssignmentsApi(RoleAssignmentsApi, metaclass=SyncClass): 75 | pass 76 | 77 | 78 | class SyncRolesApi(RolesApi, metaclass=SyncClass): 79 | pass 80 | 81 | 82 | class SyncTenantsApi(TenantsApi, metaclass=SyncClass): 83 | pass 84 | 85 | 86 | class SyncUsersApi(UsersApi, metaclass=SyncClass): 87 | pass 88 | 89 | 90 | class SyncPermitApiClient(SyncDeprecatedApi): 91 | def __init__(self, config: PermitConfig): 92 | """ 93 | Constructs a new instance of the SyncPermitApiClient class with the specified SDK configuration. 94 | 95 | Args: 96 | config: The configuration for the Permit SDK. 97 | """ 98 | super().__init__(config) 99 | 100 | self._condition_set_rules = SyncConditionSetRulesApi(config) 101 | self._condition_sets = SyncConditionSetsApi(config) 102 | self._environments = SyncEnvironmentsApi(config) 103 | self._projects = SyncProjectsApi(config) 104 | self._relationship_tuples = SyncRelationshipTuplesApi(config) 105 | self._action_groups = SyncResourceActionGroupsApi(config) 106 | self._resource_actions = SyncResourceActionsApi(config) 107 | self._resource_attributes = SyncResourceAttributesApi(config) 108 | self._resource_instances = SyncResourceInstancesApi(config) 109 | self._resource_relations = SyncResourceRelationsApi(config) 110 | self._resource_roles = SyncResourceRolesApi(config) 111 | self._resources = SyncResourcesApi(config) 112 | self._role_assignments = SyncRoleAssignmentsApi(config) 113 | self._roles = SyncRolesApi(config) 114 | self._tenants = SyncTenantsApi(config) 115 | self._users = SyncUsersApi(config) 116 | 117 | @property 118 | def condition_set_rules(self) -> SyncConditionSetRulesApi: 119 | """ 120 | API for managing condition set rules. 121 | See: https://api.permit.io/v2/redoc#tag/Condition-Set-Rules 122 | """ 123 | return self._condition_set_rules 124 | 125 | @property 126 | def condition_sets(self) -> SyncConditionSetsApi: 127 | """ 128 | API for managing condition sets. 129 | See: https://api.permit.io/v2/redoc#tag/Condition-Sets 130 | """ 131 | return self._condition_sets 132 | 133 | @property 134 | def projects(self) -> SyncProjectsApi: 135 | """ 136 | API for managing projects. 137 | See: https://api.permit.io/v2/redoc#tag/Projects 138 | """ 139 | return self._projects 140 | 141 | @property 142 | def environments(self) -> SyncEnvironmentsApi: 143 | """ 144 | API for managing environments. 145 | See: https://api.permit.io/v2/redoc#tag/Environments 146 | """ 147 | return self._environments 148 | 149 | @property 150 | def action_groups(self) -> SyncResourceActionGroupsApi: 151 | """ 152 | API for managing resource action groups. 153 | See: https://api.permit.io/v2/redoc#tag/Resource-Action-Groups 154 | """ 155 | return self._action_groups 156 | 157 | @property 158 | def resource_actions(self) -> SyncResourceActionsApi: 159 | """ 160 | API for managing resource actions. 161 | See: https://api.permit.io/v2/redoc#tag/Resource-Actions 162 | """ 163 | return self._resource_actions 164 | 165 | @property 166 | def resource_attributes(self) -> SyncResourceAttributesApi: 167 | """ 168 | API for managing resource attributes. 169 | See: https://api.permit.io/v2/redoc#tag/Resource-Attributes 170 | """ 171 | return self._resource_attributes 172 | 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 | 197 | @property 198 | def resources(self) -> SyncResourcesApi: 199 | """ 200 | API for managing resources. 201 | See: https://api.permit.io/v2/redoc#tag/Resources 202 | """ 203 | return self._resources 204 | 205 | @property 206 | def role_assignments(self) -> SyncRoleAssignmentsApi: 207 | """ 208 | API for managing role assignments. 209 | See: https://api.permit.io/v2/redoc#tag/Role-Assignments 210 | """ 211 | return self._role_assignments 212 | 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 | 221 | @property 222 | def roles(self) -> SyncRolesApi: 223 | """ 224 | API for managing roles. 225 | See: https://api.permit.io/v2/redoc#tag/Roles 226 | """ 227 | return self._roles 228 | 229 | @property 230 | def tenants(self) -> SyncTenantsApi: 231 | """ 232 | API for managing tenants. 233 | See: https://api.permit.io/v2/redoc#tag/Tenants 234 | """ 235 | return self._tenants 236 | 237 | @property 238 | def users(self) -> SyncUsersApi: 239 | """ 240 | API for managing users. 241 | See: https://api.permit.io/v2/redoc#tag/Users 242 | """ 243 | return self._users 244 | -------------------------------------------------------------------------------- /permit/config.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional 2 | 3 | from .api.context import ApiContext 4 | from .utils.pydantic_version import PYDANTIC_VERSION 5 | 6 | if PYDANTIC_VERSION < (2, 0): 7 | from pydantic import BaseModel, Field 8 | else: 9 | from pydantic.v1 import BaseModel, Field # type: ignore 10 | 11 | 12 | class LoggerConfig(BaseModel): 13 | enable: bool = Field(default=False, description="Whether or not to enable logging from the Permit library") 14 | level: str = Field(default="info", description="Sets the log level configured for the Permit SDK Logger.") 15 | label: str = Field( 16 | default="Permit", 17 | description="Sets the label configured for logs emitted by the Permit SDK Logger.", 18 | ) 19 | log_as_json: bool = Field( 20 | default=False, 21 | alias="json", 22 | description="Sets whether the SDK log output should be in JSON format.", 23 | ) 24 | 25 | 26 | class MultiTenancyConfig(BaseModel): 27 | default_tenant: str = Field( 28 | default="default", 29 | description="the key of the default tenant to be used if use_default_tenant_if_empty == True", 30 | ) 31 | use_default_tenant_if_empty: bool = Field( 32 | default=True, 33 | description="whether or not the SDK should automatically associate a resource with the defaultTenant " 34 | + "if the resource provided in permit.check() was not associated with a tenant (i.e: undefined tenant).", 35 | ) 36 | 37 | 38 | class PermitConfig(BaseModel): 39 | token: str = Field( 40 | default=..., 41 | description="The token (API Key) used for authorization against the PDP and the Permit REST API.", 42 | ) 43 | pdp: str = Field( 44 | default="http://localhost:7766", 45 | description="Configures the Policy Decision Point (PDP) url.", 46 | ) 47 | api_url: str = Field(default="https://api.permit.io", description="The url of Permit REST API") 48 | log: LoggerConfig = Field(LoggerConfig(), description="the logger configuration used by the SDK") 49 | multi_tenancy: MultiTenancyConfig = Field( 50 | MultiTenancyConfig(), 51 | description="configuration of default tenant assignment for RBAC", 52 | ) 53 | api_context: ApiContext = Field(ApiContext(), description="represents the current API key authorization level.") 54 | api_timeout: Optional[int] = Field( 55 | default=None, 56 | description="The timeout in seconds for requests to the Permit REST API.", 57 | ) 58 | pdp_timeout: Optional[int] = Field( 59 | default=None, 60 | description="The timeout in seconds for requests to the PDP.", 61 | ) 62 | proxy_facts_via_pdp: bool = Field( 63 | default=False, 64 | description="Create facts via the PDP API instead of using the default Permit REST API.", 65 | ) 66 | facts_sync_timeout: Optional[float] = Field( 67 | default=None, 68 | description="The amount of time in seconds to wait for facts to be available " 69 | "in the PDP cache before returning the response.", 70 | ) 71 | facts_sync_timeout_policy: Optional[Literal["ignore", "fail"]] = Field( 72 | default=None, 73 | description="The policy to apply when the facts sync timeout is reached.", 74 | ) 75 | 76 | class Config: 77 | arbitrary_types_allowed = True 78 | -------------------------------------------------------------------------------- /permit/enforcement/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/permit/enforcement/__init__.py -------------------------------------------------------------------------------- /permit/enforcement/interfaces.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from ..utils.pydantic_version import PYDANTIC_VERSION 4 | 5 | if PYDANTIC_VERSION < (2, 0): 6 | from pydantic import BaseModel, Field 7 | else: 8 | from pydantic.v1 import BaseModel, Field # type: ignore 9 | 10 | JWT = str 11 | 12 | 13 | class UserKey(BaseModel): 14 | key: str 15 | 16 | 17 | class AssignedRole(BaseModel): 18 | role: str # role key 19 | tenant: str # tenant key 20 | 21 | 22 | class UserInput(UserKey): 23 | first_name: Optional[str] = Field(None, alias="firstName") 24 | last_name: Optional[str] = Field(None, alias="lastName") 25 | email: Optional[str] = None 26 | roles: Optional[List[AssignedRole]] = None 27 | attributes: Optional[Dict] = None 28 | 29 | 30 | class ResourceInput(BaseModel): 31 | type: str # namespace/type of resources/objects 32 | id: Optional[str] = None # id of individual object 33 | key: Optional[str] = None # key of individual object 34 | tenant: Optional[str] = None # tenant the resource belongs to 35 | attributes: Optional[Dict] = None # extra resources attributes 36 | context: Optional[Dict] = None # extra context 37 | 38 | 39 | class OpaResult(BaseModel): 40 | allow: bool 41 | 42 | 43 | class AuthorizedUserAssignment(BaseModel): 44 | user: str = Field(..., description="The user that is authorized") 45 | tenant: str = Field(..., description="The tenant that the user is authorized for") 46 | resource: str = Field(..., description="The resource that the user is authorized for") 47 | role: str = Field(..., description="The role that the user is assigned to") 48 | 49 | 50 | AuthorizedUsersDict = Dict[str, List[AuthorizedUserAssignment]] 51 | 52 | 53 | class AuthorizedUsersResult(BaseModel): 54 | resource: str = Field( 55 | ..., 56 | description="The resource that the result is about." 57 | "Can be either 'resource:*' or 'resource:resource_instance'", 58 | ) 59 | tenant: str = Field(..., description="The tenant that the result is about") 60 | users: AuthorizedUsersDict = Field( 61 | ..., 62 | description="A key value mapping of the users that are " 63 | "authorized for the resource." 64 | "The key is the user key and the value is a list of assignments allowing the user to perform" 65 | "the requested action", 66 | ) 67 | -------------------------------------------------------------------------------- /permit/exceptions.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from typing import Optional 3 | 4 | import aiohttp 5 | from loguru import logger 6 | from typing_extensions import deprecated 7 | 8 | from permit.utils.pydantic_version import PYDANTIC_VERSION 9 | 10 | if PYDANTIC_VERSION < (2, 0): 11 | from pydantic import ValidationError 12 | else: 13 | from pydantic.v1 import ValidationError # type: ignore[assignment] 14 | 15 | from permit.api.models import ErrorDetails, HTTPValidationError 16 | 17 | DEFAULT_SUPPORT_LINK = "https://permit-io.slack.com/ssb/redirect" 18 | 19 | 20 | class PermitError(Exception): 21 | """Permit base exception""" 22 | 23 | 24 | @deprecated("Use PermitError instead") 25 | class PermitException(PermitError): # noqa: N818 26 | """Permit base exception (deprecated, use PermitError instead)""" 27 | 28 | 29 | class PermitConnectionError(PermitException): 30 | """Permit connection exception""" 31 | 32 | def __init__(self, message: str, *, error: Optional[aiohttp.ClientError] = None): 33 | super().__init__(message) 34 | self.original_error = error 35 | 36 | 37 | class PermitContextError(PermitError): 38 | """ 39 | The `PermitContextError` class represents an error that occurs when an API method 40 | is called with insufficient context (not knowing in what environment, project or 41 | organization the API call is being made). 42 | 43 | Some of the input for the API method is provided via the SDK context. 44 | If the context is missing some data required for a method - the api call will fail. 45 | """ 46 | 47 | 48 | class PermitContextChangeError(PermitError): 49 | """ 50 | The `PermitContextChangeError` will be thrown when the user is trying to set the 51 | SDK context to an object that the current API Key cannot access (and if allowed, 52 | such api calls will result is 401). Instead, the SDK throws this exception. 53 | """ 54 | 55 | 56 | class PermitApiError(PermitError): 57 | """ 58 | Wraps an error HTTP Response that occurred during a Permit REST API request. 59 | """ 60 | 61 | def __init__( 62 | self, 63 | response: aiohttp.ClientResponse, 64 | body: Optional[dict] = None, 65 | ): 66 | super().__init__() 67 | self._response = response 68 | self._body = body 69 | 70 | def _get_message(self) -> str: 71 | return f"{self.status_code} API Error: {self.details}" 72 | 73 | def __str__(self): 74 | return self._get_message() 75 | 76 | @property 77 | def message(self) -> str: 78 | return self._get_message() 79 | 80 | @property 81 | def response(self) -> aiohttp.ClientResponse: 82 | """ 83 | Get the HTTP response that returned an error status code 84 | 85 | Returns: 86 | The HTTP response object. 87 | """ 88 | return self._response 89 | 90 | @property 91 | def details(self) -> Optional[dict]: 92 | """ 93 | Get the HTTP response JSON body. Contains details about the error. 94 | 95 | Returns: 96 | The HTTP response json. If no content will return None. 97 | """ 98 | return self._body 99 | 100 | @property 101 | def request_url(self) -> str: 102 | """ 103 | Get the HTTP request URL that caused the error code. 104 | 105 | Returns: 106 | The HTTP request url 107 | """ 108 | return str(self._response.url) 109 | 110 | @property 111 | def status_code(self) -> int: 112 | """ 113 | Get the HTTP response status code 114 | 115 | Returns: 116 | The status code returned. 117 | """ 118 | return self._response.status 119 | 120 | @property 121 | def content_type(self) -> Optional[str]: 122 | """ 123 | Get the HTTP content type header of the error response. 124 | 125 | Returns: 126 | The value of the HTTP Response Content-type header, or None 127 | """ 128 | return self._response.headers.get("content-type") 129 | 130 | 131 | class PermitValidationError(PermitApiError): 132 | """ 133 | Validation error response from the Permit API. 134 | """ 135 | 136 | def __init__(self, response: aiohttp.ClientResponse, content: HTTPValidationError, body: dict): 137 | self._content = content 138 | super().__init__(response, body) 139 | 140 | def _get_message(self) -> str: 141 | message = "Validation error\n" 142 | for error in self.content.detail or []: 143 | location = " -> ".join(str(loc) for loc in error.loc) 144 | message += f"{location}\n\t{error.msg} ({error.type})\n" 145 | 146 | return message 147 | 148 | @property 149 | def content(self) -> HTTPValidationError: 150 | return self._content 151 | 152 | 153 | class PermitApiDetailedError(PermitApiError): 154 | """ 155 | Detailed error response from the Permit API. 156 | """ 157 | 158 | def __init__(self, response: aiohttp.ClientResponse, content: ErrorDetails, body: dict): 159 | self._content = content 160 | super().__init__(response, body) 161 | 162 | def _get_message(self) -> str: 163 | message = f"{self.content.title} ({self.content.error_code})\n" 164 | if self.content.message: 165 | split_message = self.content.message.replace(". ", ".\n") 166 | message += f"{split_message}\n" 167 | message += f"For more information: {self.support_link} (Request ID: {self.id})" 168 | return message 169 | 170 | @property 171 | def content(self) -> ErrorDetails: 172 | return self._content 173 | 174 | @property 175 | def id(self) -> str: 176 | return self.content.id 177 | 178 | @property 179 | def code(self) -> str: 180 | return self.content.error_code.value 181 | 182 | @property 183 | def title(self) -> str: 184 | return self.content.title 185 | 186 | @property 187 | def explanation(self) -> str: 188 | return self.content.message or "No further explanation provided" 189 | 190 | @property 191 | def support_link(self) -> str: 192 | return str(self.content.support_link or DEFAULT_SUPPORT_LINK) 193 | 194 | @property 195 | def additional_info(self): 196 | return self.content.additional_info 197 | 198 | 199 | class PermitAlreadyExistsError(PermitApiDetailedError): 200 | """ 201 | Object already exists response from the Permit API. 202 | """ 203 | 204 | 205 | class PermitNotFoundError(PermitApiDetailedError): 206 | """ 207 | Object not found response from the Permit API. 208 | """ 209 | 210 | 211 | async def handle_api_error(response: aiohttp.ClientResponse): 212 | if 200 <= response.status < 400: 213 | return 214 | 215 | try: 216 | json = await response.json() 217 | except aiohttp.ContentTypeError as e: 218 | text = await response.text() 219 | raise PermitApiError(response, {"details": text}) from e 220 | 221 | if response.status == 422: 222 | try: 223 | validation_content = HTTPValidationError.parse_obj(json) 224 | except ValidationError as e: 225 | raise PermitApiError(response, json) from e 226 | else: 227 | raise PermitValidationError(response, validation_content, json) 228 | 229 | try: 230 | content = ErrorDetails.parse_obj(json) 231 | except ValidationError as e: 232 | raise PermitApiError(response, json) from e 233 | 234 | if response.status == 409: 235 | raise PermitAlreadyExistsError(response, content, json) 236 | elif response.status == 404: 237 | raise PermitNotFoundError(response, content, json) 238 | else: 239 | raise PermitApiDetailedError(response, content, json) 240 | 241 | 242 | def handle_client_error(func): 243 | @functools.wraps(func) 244 | async def wrapped(*args, **kwargs): 245 | try: 246 | return await func(*args, **kwargs) 247 | except aiohttp.ClientError as err: 248 | logger.error(f"got client error while sending an http request:\n{err}") 249 | raise PermitConnectionError(f"{err}", error=err) from err 250 | 251 | return wrapped 252 | -------------------------------------------------------------------------------- /permit/logger.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | from .config import PermitConfig 4 | 5 | PERMIT_MODULE = "permit" 6 | 7 | 8 | def configure_logger(config: PermitConfig): 9 | if not config.log.enable: 10 | logger.disable(PERMIT_MODULE) 11 | -------------------------------------------------------------------------------- /permit/pdp_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/permit/pdp_api/__init__.py -------------------------------------------------------------------------------- /permit/pdp_api/base.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | 3 | from permit import PYDANTIC_VERSION, PermitConfig 4 | from permit.api.base import SimpleHttpClient 5 | 6 | if PYDANTIC_VERSION < (2, 0): 7 | from pydantic import BaseModel, Extra, Field 8 | else: 9 | from pydantic.v1 import BaseModel, Extra, Field # type: ignore 10 | 11 | 12 | T = TypeVar("T", bound=Callable) 13 | TModel = TypeVar("TModel", bound=BaseModel) 14 | TData = TypeVar("TData", bound=BaseModel) 15 | 16 | 17 | def pagination_params(page: int, per_page: int) -> dict: 18 | return {"page": page, "per_page": per_page} 19 | 20 | 21 | class ClientConfig(BaseModel): 22 | class Config: 23 | extra = Extra.allow 24 | 25 | base_url: str = Field( 26 | ..., 27 | description="base url that will prefix the url fragment sent via the client", 28 | ) 29 | headers: dict = Field(..., description="http headers sent to the API server") 30 | 31 | 32 | class BasePdpPermitApi: 33 | """ 34 | The base class for Permit APIs. 35 | """ 36 | 37 | def __init__(self, config: PermitConfig): 38 | """ 39 | Initialize a BasePermitApi. 40 | 41 | Args: 42 | config: The Permit SDK configuration. 43 | """ 44 | self.config = config 45 | 46 | def _build_http_client(self, endpoint_url: str = "", **kwargs): 47 | client_config = ClientConfig( 48 | base_url=f"{self.config.pdp}", 49 | headers={ 50 | "Content-Type": "application/json", 51 | "Authorization": f"bearer {self.config.token}", 52 | }, 53 | ) 54 | client_config_dict = client_config.dict() 55 | client_config_dict.update(kwargs) 56 | return SimpleHttpClient( 57 | client_config_dict, 58 | base_url=endpoint_url, 59 | ) 60 | -------------------------------------------------------------------------------- /permit/pdp_api/models.py: -------------------------------------------------------------------------------- 1 | # generated by datamodel-codegen: 2 | # filename: open.json (local PDP) 3 | # timestamp: 2024-04-09T15:36:45+00:00 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Optional 8 | 9 | from ..utils.pydantic_version import PYDANTIC_VERSION 10 | 11 | if PYDANTIC_VERSION < (2, 0): 12 | from pydantic import BaseModel, Field 13 | else: 14 | from pydantic.v1 import BaseModel, Field # type: ignore 15 | 16 | 17 | class RoleAssignment(BaseModel): 18 | user: str = Field(..., description="the user the role is assigned to", title="User") 19 | role: str = Field(..., description="the role that is assigned", title="Role") 20 | tenant: str = Field(..., description="the tenant the role is associated with", title="Tenant") 21 | resource_instance: Optional[str] = Field( 22 | None, 23 | description="the resource instance the role is associated with", 24 | title="Resource Instance", 25 | ) 26 | -------------------------------------------------------------------------------- /permit/pdp_api/pdp_api_client.py: -------------------------------------------------------------------------------- 1 | from permit.utils.sync import SyncClass 2 | 3 | from ..config import PermitConfig 4 | from .role_assignments import RoleAssignmentsApi 5 | 6 | 7 | class SyncRoleAssignmentsApi(RoleAssignmentsApi, metaclass=SyncClass): 8 | pass 9 | 10 | 11 | class PermitPdpApiClient: 12 | def __init__(self, config: PermitConfig): 13 | """ 14 | Constructs a new instance of the PdpApiClient class with the specified SDK configuration. 15 | 16 | Args: 17 | config: The configuration for the Permit SDK. 18 | """ 19 | self._config = config 20 | self._headers = { 21 | "Content-Type": "application/json", 22 | "Authorization": f"bearer {self._config.token}", 23 | } 24 | self._base_url = self._config.pdp 25 | 26 | self._role_assignments = RoleAssignmentsApi(config) 27 | 28 | @property 29 | def role_assignments(self) -> RoleAssignmentsApi: 30 | return self._role_assignments 31 | 32 | 33 | class SyncPDPApi(PermitPdpApiClient): 34 | def __init__(self, config: PermitConfig): 35 | self._role_assignments = SyncRoleAssignmentsApi(config) 36 | 37 | @property 38 | def role_assignments(self) -> SyncRoleAssignmentsApi: 39 | return self._role_assignments # type: ignore[return-value] 40 | -------------------------------------------------------------------------------- /permit/pdp_api/role_assignments.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from permit import PYDANTIC_VERSION 4 | from permit.api.base import SimpleHttpClient 5 | from permit.pdp_api.base import BasePdpPermitApi, pagination_params 6 | from permit.pdp_api.models import RoleAssignment 7 | 8 | if PYDANTIC_VERSION < (2, 0): 9 | from pydantic import validate_arguments 10 | else: 11 | from pydantic.v1 import validate_arguments 12 | 13 | 14 | class RoleAssignmentsApi(BasePdpPermitApi): 15 | @property 16 | def __role_assignments(self) -> SimpleHttpClient: 17 | return self._build_http_client("/local/role_assignments") 18 | 19 | @validate_arguments # type: ignore[operator] 20 | async def list( 21 | self, 22 | user_key: Optional[str] = None, 23 | role_key: Optional[str] = None, 24 | tenant_key: Optional[str] = None, 25 | resource_key: Optional[str] = None, 26 | resource_instance_key: Optional[str] = None, 27 | page: int = 1, 28 | per_page: int = 100, 29 | ) -> List[RoleAssignment]: 30 | """ 31 | Retrieves a list of role assignments based on the specified filters. 32 | 33 | Args: 34 | user_key: optional user filter, will only return role assignments granted to this user. 35 | role_key: optional role filter, will only return role assignments granting this role. 36 | tenant_key: optional tenant filter, will only return role assignments granted in that tenant. 37 | resource_key: optional resource type filter, will only return role assignments granted on that resource type. 38 | resource_instance_key: optional resource instance filter, will only return role assignments granted on that resource instance. 39 | page: The page number to fetch (default: 1). 40 | per_page: How many items to fetch per page (default: 100). 41 | 42 | Returns: 43 | an array of role assignments. 44 | 45 | Raises: 46 | PermitApiError: If the API returns an error HTTP status code. 47 | PermitContextError: If the configured ApiContext does not match the required endpoint context. 48 | """ # noqa: E501 49 | params = pagination_params(page, per_page) 50 | if user_key is not None: 51 | params.update(user=user_key) 52 | if role_key is not None: 53 | params.update(role=role_key) 54 | if tenant_key is not None: 55 | params.update(tenant=tenant_key) 56 | if resource_key is not None: 57 | params.update(resource=resource_key) 58 | if resource_instance_key is not None: 59 | params.update(resource_instance=resource_instance_key) 60 | return await self.__role_assignments.get( 61 | "", 62 | model=List[RoleAssignment], 63 | params=params, 64 | ) 65 | -------------------------------------------------------------------------------- /permit/permit.py: -------------------------------------------------------------------------------- 1 | import json 2 | from contextlib import contextmanager 3 | from typing import Any, Dict, Generator, List, Literal, Optional 4 | 5 | from loguru import logger 6 | from typing_extensions import Self 7 | 8 | from .api.api_client import PermitApiClient 9 | from .api.elements import ElementsApi 10 | from .config import PermitConfig 11 | from .enforcement.enforcer import ( 12 | Action, 13 | AuthorizedUsersResult, 14 | CheckQuery, 15 | Enforcer, 16 | Resource, 17 | User, 18 | ) 19 | from .logger import configure_logger 20 | from .pdp_api.pdp_api_client import PermitPdpApiClient 21 | from .utils.context import Context 22 | 23 | 24 | class Permit: 25 | def __init__(self, config: Optional[PermitConfig] = None, **options): 26 | self._config: PermitConfig = config if config is not None else PermitConfig(**options) 27 | 28 | configure_logger(self._config) 29 | self._enforcer = Enforcer(self._config) 30 | self._api = PermitApiClient(self._config) 31 | self._elements = ElementsApi(self._config) 32 | self._pdp_api = PermitPdpApiClient(self._config) 33 | logger.debug( 34 | "Permit SDK initialized with config:\n${}", 35 | json.dumps(self._config.dict(exclude={"api_context"})), 36 | ) 37 | 38 | @property 39 | def config(self): 40 | """ 41 | Access the SDK configuration using this property. 42 | Once the SDK is initialized, the configuration is read-only. 43 | 44 | Usage example: 45 | 46 | permit = Permit(config) 47 | pdp_url = permit.config.pdp 48 | """ 49 | return self._config.copy() 50 | 51 | @contextmanager 52 | def wait_for_sync( 53 | self, timeout: float = 10.0, policy: Optional[Literal["ignore", "fail"]] = None 54 | ) -> Generator[Self, None, None]: 55 | """ 56 | Context manager that returns a client that is configured 57 | to wait for facts to be synced before proceeding. 58 | 59 | 60 | Args: 61 | timeout: The amount of time in seconds to wait for facts to be available in the PDP 62 | cache before returning the response. 63 | policy: Weather to fail the request when the timeout is reached or ignore. 64 | 65 | Set None to keep the default policy set in the instance config or the default value of PDP. 66 | 67 | Yields: 68 | Permit: A Permit instance that is configured to wait for facts to be synced. 69 | 70 | See Also: 71 | https://docs.permit.io/how-to/manage-data/local-facts-uploader 72 | """ 73 | if not self._config.proxy_facts_via_pdp: 74 | logger.warning("Tried to wait for synced facts but proxy_facts_via_pdp is disabled, ignoring...") 75 | yield self 76 | return 77 | contextualized_config = self.config # this copies the config 78 | contextualized_config.facts_sync_timeout = timeout 79 | if policy is not None: 80 | contextualized_config.facts_sync_timeout_policy = policy 81 | yield self.__class__(contextualized_config) 82 | 83 | @property 84 | def api(self) -> PermitApiClient: 85 | """ 86 | Access the Permit REST API using this property. 87 | 88 | Usage example: 89 | 90 | permit = Permit(token="") 91 | await permit.api.roles.create(...) 92 | """ 93 | return self._api 94 | 95 | @property 96 | def elements(self) -> ElementsApi: 97 | """ 98 | Access the Permit Elements API using this property. 99 | 100 | Usage example: 101 | 102 | permit = Permit(token="") 103 | await permit.elements.loginAs(user, tenant) 104 | """ 105 | return self._elements 106 | 107 | @property 108 | def pdp_api(self) -> PermitPdpApiClient: 109 | """ 110 | Access the Permit PDP API using this property. 111 | 112 | Usage example: 113 | 114 | permit = Permit(token="") 115 | await permit.pdp_api.role_assignments.list() 116 | """ 117 | return self._pdp_api 118 | 119 | async def authorized_users( 120 | self, 121 | action: Action, 122 | resource: Resource, 123 | context: Optional[Context] = None, 124 | ) -> AuthorizedUsersResult: 125 | """ 126 | Queries to get all the users that are authorized to perform an action on a resource within the specified context. 127 | 128 | Args: 129 | action: The action to be performed on the resource. 130 | resource: The resource object representing the resource. 131 | context: The context object representing the context in which the action is performed. Defaults to None. 132 | 133 | Returns: 134 | AuthorizedUsersResult: Contains all the authorized users and the role assignments that granted the permission. 135 | 136 | Raises: 137 | PermitConnectionError: If an error occurs while sending the authorization request to the PDP. 138 | 139 | Examples: 140 | 141 | # all the users that can close any issue? 142 | await permit.authorized_users('close', 'issue') 143 | 144 | # all the users that can close an issue who's id is 1234? 145 | await permit.authorized_users('close', 'issue:1234') 146 | 147 | # all the users that can close (any) issues belonging to the 't1' tenant? 148 | # (in a multi tenant application) 149 | await permit.authorized_users('close', {'type': 'issue', 'tenant': 't1'}) 150 | """ # noqa: E501 151 | return await self._enforcer.authorized_users(action, resource, context) 152 | 153 | async def bulk_check( 154 | self, 155 | checks: List[CheckQuery], 156 | context: Optional[Context] = None, 157 | ) -> List[bool]: 158 | """ 159 | Checks if a user is authorized to perform an action on a list of resources within the specified context. 160 | 161 | Args: 162 | checks: A list of check queries, each query contain user, action, and resource. 163 | context: The context object representing the context in which the action is performed. Defaults to None. 164 | 165 | Returns: 166 | list[bool]: A list of booleans indicating whether the user is authorized for each resource. 167 | 168 | Raises: 169 | PermitConnectionError: If an error occurs while sending the authorization request to the PDP. 170 | 171 | Examples: 172 | 173 | # Bulk query of multiple check conventions 174 | await permit.bulk_check([ 175 | { 176 | "user": user, 177 | "action": "close", 178 | "resource": {type: "issue", key: "1234"}, 179 | }, 180 | { 181 | "user": {key: "user"}, 182 | "action": "close", 183 | "resource": "issue:1235", 184 | }, 185 | { 186 | "user": "user_a", 187 | "action": "close", 188 | "resource": "issue", 189 | }, 190 | ]) 191 | """ 192 | return await self._enforcer.bulk_check(checks, context) 193 | 194 | async def check( 195 | self, 196 | user: User, 197 | action: Action, 198 | resource: Resource, 199 | context: Optional[Context] = None, 200 | ) -> bool: 201 | """ 202 | Checks if a user is authorized to perform an action on a resource within the specified context. 203 | 204 | Args: 205 | user: The user object representing the user. 206 | action: The action to be performed on the resource. 207 | resource: The resource object representing the resource. 208 | context: The context object representing the context in which the action is performed. Defaults to None. 209 | 210 | Returns: 211 | bool: True if the user is authorized, False otherwise. 212 | 213 | Raises: 214 | PermitConnectionError: If an error occurs while sending the authorization request to the PDP. 215 | 216 | Examples: 217 | 218 | # can the user close any issue? 219 | await permit.check(user, 'close', 'issue') 220 | 221 | # can the user close any issue who's id is 1234? 222 | await permit.check(user, 'close', 'issue:1234') 223 | 224 | # can the user close (any) issues belonging to the 't1' tenant? 225 | # (in a multi tenant application) 226 | await permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) 227 | """ 228 | return await self._enforcer.check(user, action, resource, context) 229 | 230 | async def get_user_permissions( 231 | self, 232 | user: User, 233 | tenants: Optional[List[str]] = None, 234 | resources: Optional[List[str]] = None, 235 | resource_types: Optional[List[str]] = None, 236 | ) -> dict: 237 | """ 238 | Get all permissions for a user. 239 | 240 | Args: 241 | user: The user object or user key 242 | tenants: Optional list of tenants to filter permissions 243 | resources: Optional list of resources to filter 244 | resource_types: Optional list of resource types to filter 245 | config: Optional configuration dictionary 246 | 247 | Returns: 248 | dict: User permissions per tenant 249 | 250 | Raises: 251 | PermitConnectionError: If an error occurs while sending the request to the PDP 252 | """ 253 | return await self._enforcer.get_user_permissions(user, tenants, resources, resource_types) 254 | 255 | async def filter_objects( 256 | self, user: User, action: Action, context: Context, resources: List[Dict[str, Any]] 257 | ) -> List[Dict[str, Any]]: 258 | """ 259 | Get all permissions for a user. 260 | 261 | Args: 262 | user: The user object or user key 263 | tenants: Optional list of tenants to filter permissions 264 | resources: Optional list of resources to filter 265 | resource_types: Optional list of resource types to filter 266 | config: Optional configuration dictionary 267 | 268 | Returns: 269 | dict: User permissions per tenant 270 | 271 | Raises: 272 | PermitConnectionError: If an error occurs while sending the request to the PDP 273 | """ 274 | return await self._enforcer.filter_objects(user, action, context, resources) 275 | -------------------------------------------------------------------------------- /permit/sync.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from .api.elements import SyncElementsApi 4 | from .api.sync_api_client import SyncPermitApiClient 5 | from .config import PermitConfig 6 | from .enforcement.enforcer import Action, CheckQuery, Resource, SyncEnforcer, User 7 | from .pdp_api.pdp_api_client import SyncPDPApi 8 | from .permit import Permit as AsyncPermit 9 | from .utils.context import Context 10 | 11 | 12 | class Permit(AsyncPermit): 13 | def __init__(self, config: Optional[PermitConfig] = None, **options): 14 | super().__init__(config, **options) 15 | self._enforcer = SyncEnforcer(self._config) 16 | self._api = SyncPermitApiClient(self._config) # type: ignore[assignment] 17 | self._elements = SyncElementsApi(self._config) 18 | self._pdp_api = SyncPDPApi(self._config) 19 | 20 | @property 21 | def api(self) -> SyncPermitApiClient: # type: ignore[override] 22 | """ 23 | Access the Permit REST API using this property. 24 | 25 | Usage example: 26 | 27 | permit = Permit(token="") 28 | permit.api.roles.create(...) 29 | """ 30 | return self._api # type: ignore[return-value] 31 | 32 | @property 33 | def elements(self) -> SyncElementsApi: 34 | """ 35 | Access the Permit Elements API using this property. 36 | 37 | Usage example: 38 | 39 | permit = Permit(token="") 40 | permit.elements.loginAs(user, tenant) 41 | """ 42 | return self._elements # type: ignore[return-value] 43 | 44 | @property 45 | def pdp_api(self) -> SyncPDPApi: 46 | """ 47 | Access the Permit PDP API using this property. 48 | 49 | Usage example: 50 | permit = Permit(token="") 51 | permit.pdp_api.role_assignments(...) 52 | """ 53 | return self._pdp_api # type: ignore[return-value] 54 | 55 | def bulk_check( # type: ignore[override] 56 | self, 57 | checks: List[CheckQuery], 58 | context: Optional[Context] = None, 59 | ) -> List[bool]: 60 | """ 61 | Checks if a user is authorized to perform an action on a list of resources within the specified context. 62 | 63 | Args: 64 | checks: A list of CheckQuery objects representing the authorization checks to be performed. 65 | context: The context object representing the context in which the action is performed. Defaults to None. 66 | 67 | Returns: 68 | list[bool]: A list of booleans indicating whether the user is authorized for each resource. 69 | 70 | Raises: 71 | PermitConnectionError: If an error occurs while sending the authorization request to the PDP. 72 | 73 | Examples: 74 | 75 | # Bulk query of multiple check conventions 76 | await permit.bulk_check([ 77 | { 78 | "user": user, 79 | "action": "close", 80 | "resource": {type: "issue", key: "1234"}, 81 | }, 82 | { 83 | "user": {key: "user"}, 84 | "action": "close", 85 | "resource": "issue:1235", 86 | }, 87 | { 88 | "user": "user_a", 89 | "action": "close", 90 | "resource": "issue", 91 | }, 92 | ]) 93 | """ 94 | return self._enforcer.bulk_check(checks, context) # type: ignore[return-value] 95 | 96 | def check( # type: ignore[override] 97 | self, 98 | user: User, 99 | action: Action, 100 | resource: Resource, 101 | context: Optional[Context] = None, 102 | ) -> bool: 103 | """ 104 | Checks if a user is authorized to perform an action on a resource within the specified context. 105 | 106 | Args: 107 | user: The user object representing the user. 108 | action: The action to be performed on the resource. 109 | resource: The resource object representing the resource. 110 | context: The context object representing the context in which the action is performed. Defaults to None. 111 | 112 | Returns: 113 | bool: True if the user is authorized, False otherwise. 114 | 115 | Raises: 116 | PermitConnectionError: If an error occurs while sending the authorization request to the PDP. 117 | 118 | Examples: 119 | 120 | # can the user close any issue? 121 | permit.check(user, 'close', 'issue') 122 | 123 | # can the user close any issue who's id is 1234? 124 | permit.check(user, 'close', 'issue:1234') 125 | 126 | # can the user close (any) issues belonging to the 't1' tenant? 127 | # (in a multi tenant application) 128 | permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) 129 | """ 130 | return self._enforcer.check(user, action, resource, context) # type: ignore[return-value] 131 | -------------------------------------------------------------------------------- /permit/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/permit/utils/__init__.py -------------------------------------------------------------------------------- /permit/utils/context.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Dict, List 2 | 3 | from .dicts import deep_merge 4 | 5 | Context = Dict[str, Any] 6 | ContextTransform = Callable[[Context], Context] 7 | 8 | 9 | class ContextStore: 10 | def __init__(self): 11 | self._base_context: Context = {} 12 | self._transforms: List[ContextTransform] = [] 13 | 14 | def add(self, context: Context): 15 | self._base_context = deep_merge(self._base_context, context) 16 | 17 | def register_transform(self, transform: ContextTransform): 18 | self._transforms.append(transform) 19 | 20 | def get_derived_context(self, context: Context) -> Context: 21 | return deep_merge(self._base_context, context) 22 | 23 | def transform(self, initial_context: Context) -> Context: 24 | context = initial_context.copy() 25 | for transform in self._transforms: 26 | context = transform(context) 27 | return context 28 | -------------------------------------------------------------------------------- /permit/utils/deprecation.py: -------------------------------------------------------------------------------- 1 | from asyncio import iscoroutinefunction 2 | from functools import wraps 3 | from warnings import warn 4 | 5 | 6 | def deprecated(message: str): 7 | def decorator(func): 8 | @wraps(func) 9 | def wrapper(*args, **kwargs): 10 | warn(message, DeprecationWarning, stacklevel=2) 11 | return func(*args, **kwargs) 12 | 13 | @wraps(func) 14 | async def async_wrapper(*args, **kwargs): 15 | warn(message, DeprecationWarning, stacklevel=2) 16 | return await func(*args, **kwargs) 17 | 18 | if iscoroutinefunction(func): 19 | return async_wrapper 20 | else: 21 | return wrapper 22 | 23 | return decorator 24 | -------------------------------------------------------------------------------- /permit/utils/dicts.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing import Dict 3 | 4 | 5 | def deep_merge(base: Dict, overrides: Dict): 6 | """ 7 | merges two dicts recursively 8 | """ 9 | result = base.copy() # create a clean copy of base 10 | for key in overrides: 11 | if key not in result or not isinstance(result[key], dict): 12 | result[key] = deepcopy(overrides[key]) 13 | else: 14 | result[key] = deep_merge(result[key], overrides[key]) 15 | return result 16 | -------------------------------------------------------------------------------- /permit/utils/pydantic_version.py: -------------------------------------------------------------------------------- 1 | import pydantic 2 | 3 | PYDANTIC_VERSION = tuple(map(int, pydantic.__version__.split("."))) 4 | -------------------------------------------------------------------------------- /permit/utils/sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | from asyncio import iscoroutinefunction 4 | from functools import wraps 5 | from typing import Any, Awaitable, Callable, Coroutine, TypeVar 6 | 7 | from typing_extensions import ParamSpec, TypeGuard 8 | 9 | P = ParamSpec("P") 10 | T = TypeVar("T") 11 | 12 | 13 | def run_coroutine_sync(coroutine: Coroutine[Any, Any, T]) -> T: 14 | try: 15 | loop = asyncio.get_running_loop() 16 | except RuntimeError: 17 | return asyncio.run(coroutine) 18 | 19 | if threading.current_thread() is threading.main_thread(): 20 | return loop.run_until_complete(coroutine) 21 | else: 22 | return asyncio.run_coroutine_threadsafe(coroutine, loop).result() 23 | 24 | 25 | def async_to_sync(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: 26 | @wraps(func) 27 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 28 | return run_coroutine_sync(func(*args, **kwargs)) 29 | 30 | return wrapper 31 | 32 | 33 | def iscoroutine_func(callable: Callable) -> TypeGuard[Callable[..., Awaitable]]: 34 | return iscoroutinefunction(callable) 35 | 36 | 37 | class SyncClass(type): 38 | def __new__(cls, name, bases, class_dict): 39 | class_obj = super().__new__(cls, name, bases, class_dict) 40 | 41 | for name in dir(class_obj): 42 | if name.startswith("_"): 43 | # do not monkey-patch protected or private method 44 | continue 45 | 46 | attr = getattr(class_obj, name) 47 | if attr.__class__.__name__ in ("cython_function_or_method", "function"): 48 | # Handle cython method 49 | is_coroutine = True 50 | else: 51 | is_coroutine = iscoroutine_func(attr) 52 | if callable(attr) and is_coroutine: 53 | # monkey-patch public method using async_to_sync decorator 54 | setattr(class_obj, name, async_to_sync(attr)) 55 | 56 | return class_obj 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | src = ["permit"] 4 | exclude = ["permit/api/models.py"] 5 | target-version = "py38" 6 | 7 | [tool.ruff.lint] 8 | select = [ 9 | "E", # pycodestyle 10 | "W", # pycodestyle 11 | "F", # pyflakes 12 | "N", # pep8 13 | "I", # isort 14 | "BLE", # flake8 blind except 15 | "FBT", # flake8 boolean trap 16 | "B", # flake8 bug bear 17 | "C4", # flake8 comprehensions 18 | "PIE", # flake8 pie 19 | "T20", # flake8 print 20 | "SIM", # flake8 simplify 21 | "ARG", # flake8 unused arguments 22 | "PTH", # flake8 pathlib 23 | "ASYNC", # flake8 Asyncio rules 24 | # "UP", # pyupgrade 25 | "ERA", # comment out code 26 | "RUF", # ruff rules 27 | "FAST", # FastAPI rules 28 | ] 29 | 30 | [tool.ruff.lint.flake8-tidy-imports] 31 | ban-relative-imports = "all" 32 | 33 | [tool.mypy] 34 | python_version = "3.8" 35 | packages = ["permit"] 36 | plugins = ["pydantic.mypy"] 37 | 38 | check_untyped_defs = true 39 | warn_unused_configs = true 40 | warn_redundant_casts = true 41 | warn_unused_ignores = true 42 | warn_unreachable = true 43 | 44 | [[tool.mypy.overrides]] 45 | module = ["permit.api.models"] 46 | ignore_errors = true 47 | 48 | [[tool.mypy.overrides]] 49 | module = ["tests"] 50 | ignore_errors = true 51 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = auto 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-asyncio 3 | pytest-cov 4 | pytest-mock 5 | aioresponses 6 | # datamodel-code-generator>=0.19.0,<1 7 | pytest_httpserver 8 | # to solve snyk issue 9 | werkzeug>=2.3.8 10 | zipp>=3.19.1 11 | aiohttp>=3.8.4,<4 12 | ruff 13 | mypy 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.8.4,<4 2 | httpx>=0.24.1,<1 3 | loguru>=0.7.0,<1 4 | pydantic[email]>=1.10.7 5 | typing-extensions>=4.5.0,<5 6 | zipp>=3.19.1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def get_requirements(env=""): 7 | if env: 8 | env = f"-{env}" 9 | with Path(f"requirements{env}.txt").open() as fp: 10 | return [x.strip() for x in fp.read().split("\n") if not x.startswith("#")] 11 | 12 | 13 | def get_readme() -> str: 14 | this_directory = Path(__file__).parent 15 | long_description = (this_directory / "README.md").read_text() 16 | return long_description 17 | 18 | 19 | setup( 20 | name="permit", 21 | version="2.6.5", 22 | packages=find_packages(), 23 | author="Asaf Cohen", 24 | author_email="asaf@permit.io", 25 | license="Apache 2.0", 26 | python_requires=">=3.8", 27 | description="Permit.io python sdk", 28 | install_requires=get_requirements(), 29 | long_description=get_readme(), 30 | long_description_content_type="text/markdown", 31 | classifiers=[ 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/permitio/permit-python/fb88993cdebf4cbea217038464924895c3743cb6/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from permit import Permit, PermitConfig 6 | from permit.sync import Permit as SyncPermit 7 | 8 | 9 | @pytest.fixture 10 | def permit_config() -> PermitConfig: 11 | default_pdp_address = ( 12 | "https://cloudpdp.api.permit.io" if os.getenv("CLOUD_PDP") == "true" else "http://localhost:7766" 13 | ) 14 | default_api_address = "https://api.permit.io" if os.getenv("API_TIER") == "prod" else "http://localhost:8000" 15 | 16 | token = os.getenv("PDP_API_KEY", "") 17 | pdp_address = os.getenv("PDP_URL", default_pdp_address) 18 | api_url = os.getenv("PDP_CONTROL_PLANE", default_api_address) 19 | 20 | if not token: 21 | pytest.fail("PDP_API_KEY is not configured, test cannot run!") 22 | 23 | return PermitConfig( 24 | token=token, 25 | pdp=pdp_address, 26 | api_url=api_url, 27 | log={ 28 | "level": "debug", 29 | "enable": True, 30 | }, 31 | ) 32 | 33 | 34 | @pytest.fixture 35 | def permit(permit_config: PermitConfig) -> Permit: 36 | return Permit(permit_config) 37 | 38 | 39 | @pytest.fixture 40 | def sync_permit(permit_config: PermitConfig) -> SyncPermit: 41 | return SyncPermit(permit_config) 42 | 43 | 44 | @pytest.fixture 45 | def permit_config_cloud() -> PermitConfig: 46 | token = os.getenv("PDP_API_KEY", "") 47 | pdp_address = os.getenv("PDP_URL", "https://cloudpdp.api.permit.io") 48 | api_url = os.getenv("PDP_CONTROL_PLANE", "https://api.permit.io") 49 | 50 | if not token: 51 | pytest.fail("PDP_API_KEY is not configured, test cannot run!") 52 | 53 | return PermitConfig( 54 | token=token, 55 | pdp=pdp_address, 56 | api_url=api_url, 57 | log={ 58 | "level": "debug", 59 | "enable": True, 60 | }, 61 | ) 62 | 63 | 64 | @pytest.fixture 65 | def permit_cloud(permit_config_cloud: PermitConfig) -> Permit: 66 | return Permit(permit_config_cloud) 67 | -------------------------------------------------------------------------------- /tests/endpoints/test_bulk_operations.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from loguru import logger 4 | 5 | from permit import Permit, RoleCreate, TenantCreate, UserCreate 6 | from permit.api.models import ( 7 | ResourceCreate, 8 | ResourceInstanceCreate, 9 | RoleAssignmentCreate, 10 | ) 11 | from permit.exceptions import PermitAlreadyExistsError 12 | 13 | # Schema ---------------------------------------------------------------- 14 | EDITOR = "editor" 15 | VIEWER = "viewer" 16 | 17 | ACCOUNT = ResourceCreate( 18 | key="Account", 19 | name="Account", 20 | urn="prn:gdrive:account", 21 | description="a google drive account", 22 | actions={ 23 | "view": {}, 24 | "create": {}, 25 | "delete": {}, 26 | "update": {}, 27 | }, 28 | # tests creation of resource roles as part of the resource 29 | roles={ 30 | EDITOR: { 31 | "name": "Admin", 32 | "permissions": [ 33 | "view", 34 | "create", 35 | "delete", 36 | "update", 37 | ], 38 | }, 39 | VIEWER: { 40 | "name": "Member", 41 | "permissions": [ 42 | "view", 43 | ], 44 | }, 45 | }, 46 | ) 47 | 48 | USER_A = UserCreate( 49 | key=str(uuid.uuid4()), 50 | email="asaf@permit.io", 51 | first_name="Asaf", 52 | last_name="Cohen", 53 | attributes={"age": 35}, 54 | ) 55 | USER_B = UserCreate( 56 | key=str(uuid.uuid4()), 57 | email="john@permit.io", 58 | first_name="John", 59 | last_name="Doe", 60 | attributes={"age": 27}, 61 | ) 62 | USER_C = UserCreate( 63 | key=str(uuid.uuid4()), 64 | email="jane@permit.io", 65 | first_name="Jane", 66 | last_name="Doe", 67 | attributes={"age": 25}, 68 | ) 69 | 70 | TENANT_1 = TenantCreate(key=str(uuid.uuid4()), name="Tenant 1") 71 | TENANT_2 = TenantCreate(key=str(uuid.uuid4()), name="Tenant 2") 72 | 73 | ADMIN = RoleCreate( 74 | key="admin", 75 | name="Admin", 76 | permissions=[ 77 | f"{ACCOUNT.key}:view", 78 | f"{ACCOUNT.key}:create", 79 | f"{ACCOUNT.key}:delete", 80 | f"{ACCOUNT.key}:update", 81 | ], 82 | ) 83 | MEMBER = RoleCreate(key="member", name="Member") 84 | 85 | CREATED_USERS = [USER_A, USER_B, USER_C] 86 | CREATED_TENANTS = [TENANT_1, TENANT_2] 87 | 88 | ACC1 = ResourceInstanceCreate( 89 | resource=ACCOUNT.key, 90 | key=str(uuid.uuid4()), 91 | tenant=TENANT_1.key, 92 | ) 93 | 94 | ACC2 = ResourceInstanceCreate( 95 | resource=ACCOUNT.key, 96 | key=str(uuid.uuid4()), 97 | tenant=TENANT_1.key, 98 | ) 99 | 100 | ACC3 = ResourceInstanceCreate( 101 | resource=ACCOUNT.key, 102 | key=str(uuid.uuid4()), 103 | tenant=TENANT_2.key, 104 | ) 105 | 106 | CREATED_RESOURCE_INSTANCES = [ACC1, ACC2, ACC3] 107 | 108 | CREATED_ASSIGNMENTS = [ 109 | # admin on tenant 110 | RoleAssignmentCreate( 111 | user=USER_A.key, 112 | role=ADMIN.key, 113 | tenant=TENANT_1.key, 114 | ), 115 | # resource instance roles 116 | RoleAssignmentCreate( 117 | user=USER_A.key, 118 | role=EDITOR, 119 | tenant=TENANT_1.key, 120 | resource_instance=f"{ACC1.resource}:{ACC1.key}", 121 | ), 122 | RoleAssignmentCreate( 123 | user=USER_B.key, 124 | role=VIEWER, 125 | tenant=TENANT_1.key, 126 | resource_instance=f"{ACC2.resource}:{ACC2.key}", 127 | ), 128 | # this instance will be created implicitly 129 | RoleAssignmentCreate( 130 | user=USER_C.key, 131 | role=VIEWER, 132 | tenant=TENANT_2.key, 133 | resource_instance=f"{ACC3.resource}:{ACC3.key}", 134 | ), 135 | ] 136 | 137 | 138 | async def test_bulk_operations(permit: Permit): 139 | ## create resource and global role ------------------------------------ 140 | try: 141 | resource = await permit.api.resources.create(ACCOUNT) 142 | assert resource is not None 143 | assert resource.key == ACCOUNT.key 144 | except PermitAlreadyExistsError: 145 | logger.info("Account resource already exists...") 146 | 147 | try: 148 | role = await permit.api.roles.create(ADMIN) 149 | assert role is not None 150 | assert role.key == ADMIN.key 151 | except PermitAlreadyExistsError: 152 | logger.info("Admin role already exists...") 153 | 154 | ## bulk create tenants ------------------------------------ 155 | 156 | # initial number of tenants 157 | tenants = await permit.api.tenants.list() 158 | len_tenants_original = len(tenants) 159 | 160 | # create tenants in bulk 161 | await permit.api.tenants.bulk_create(CREATED_TENANTS) 162 | 163 | # check increased number of tenants 164 | tenants = await permit.api.tenants.list() 165 | assert len(tenants) == len_tenants_original + len(CREATED_TENANTS) 166 | 167 | ## bulk create users ------------------------------------ 168 | 169 | # initial number of users 170 | users = (await permit.api.users.list()).data 171 | len_users_original = len(users) 172 | 173 | # create users in bulk 174 | await permit.api.users.bulk_create(CREATED_USERS) 175 | 176 | # check increased number of users 177 | users = (await permit.api.users.list()).data 178 | assert len(users) == len_users_original + len(CREATED_USERS) 179 | 180 | ## bulk create resource instances ------------------------------------ 181 | # initial number of users 182 | instances = await permit.api.resource_instances.list() 183 | len_instances_original = len(instances) 184 | 185 | # create instances in bulk (keep one to create implicitly by role assignment) 186 | await permit.api.resource_instances.bulk_replace(CREATED_RESOURCE_INSTANCES[:-1]) 187 | 188 | # check increased number of instances 189 | instances = await permit.api.resource_instances.list() 190 | assert len(instances) == len_instances_original + len(CREATED_RESOURCE_INSTANCES) - 1 191 | 192 | ## bulk create role assignments ------------------------------------ 193 | 194 | # initial number of role assignments 195 | assignments = await permit.api.role_assignments.list() 196 | len_assignments_original = len(assignments) 197 | 198 | # create role assignments in bulk 199 | await permit.api.role_assignments.bulk_assign(CREATED_ASSIGNMENTS) 200 | 201 | # check increased number of role assignments 202 | assignments = await permit.api.role_assignments.list() 203 | assert len(assignments) == len_assignments_original + len(CREATED_ASSIGNMENTS) 204 | 205 | # check that instance created implicitly 206 | instances = await permit.api.resource_instances.list() 207 | assert len(instances) == len_instances_original + len(CREATED_RESOURCE_INSTANCES) 208 | 209 | ## bulk delete resource instances ----------------------------------- 210 | await permit.api.resource_instances.bulk_delete( 211 | [f"{inst.resource}:{inst.key}" for inst in CREATED_RESOURCE_INSTANCES] 212 | ) 213 | 214 | instances = await permit.api.resource_instances.list() 215 | assert len(instances) == len_instances_original 216 | 217 | assignments = await permit.api.role_assignments.list() 218 | assert len(assignments) == len_assignments_original + 1 # (tenant role) 219 | 220 | ## bulk delete users ----------------------------------- 221 | await permit.api.users.bulk_delete([user.key for user in CREATED_USERS]) 222 | 223 | users = (await permit.api.users.list()).data 224 | assert len(users) == len_users_original 225 | 226 | assignments = await permit.api.role_assignments.list() 227 | assert len(assignments) == len_assignments_original + 1 # (tenant role) 228 | 229 | ## bulk delete tenants ----------------------------------- 230 | await permit.api.tenants.bulk_delete([tenant.key for tenant in CREATED_TENANTS]) 231 | 232 | tenants = await permit.api.tenants.list() 233 | assert len(tenants) == len_tenants_original 234 | -------------------------------------------------------------------------------- /tests/endpoints/test_envs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import pytest 5 | from loguru import logger 6 | from tests.utils import handle_api_error 7 | 8 | from permit import Permit 9 | from permit.api.context import ApiKeyAccessLevel 10 | from permit.api.models import ( 11 | EnvironmentCreate, 12 | EnvironmentRead, 13 | ProjectCreate, 14 | ProjectRead, 15 | ) 16 | from permit.config import PermitConfig 17 | from permit.exceptions import PermitApiError, PermitConnectionError, PermitContextError 18 | 19 | CREATED_PROJECTS = [ProjectCreate(key="test-python-proj", name="New Python Project")] 20 | CREATED_ENVIRONMENTS = [ 21 | EnvironmentCreate(key="my-python-env", name="My Python Env"), 22 | EnvironmentCreate(key="my-python-env-2", name="My Python Env 2"), 23 | ] 24 | 25 | 26 | @pytest.fixture 27 | def permit_with_org_level_api_key() -> Permit: 28 | token = os.getenv("ORG_PDP_API_KEY", "") 29 | pdp_address = os.getenv("PDP_URL", "http://localhost:7766") 30 | api_url = os.getenv("PDP_CONTROL_PLANE", "https://api.permit.io") 31 | 32 | return Permit( 33 | PermitConfig( 34 | token=token, 35 | pdp=pdp_address, 36 | api_url=api_url, 37 | log={ 38 | "level": "debug", 39 | "enable": True, 40 | }, 41 | ) 42 | ) 43 | 44 | 45 | @pytest.fixture 46 | def permit_with_project_level_api_key() -> Permit: 47 | token = os.getenv("PROJECT_PDP_API_KEY", "") 48 | pdp_address = os.getenv("PDP_URL", "http://localhost:7766") 49 | api_url = os.getenv("PDP_CONTROL_PLANE", "https://api.permit.io") 50 | 51 | return Permit( 52 | PermitConfig( 53 | token=token, 54 | pdp=pdp_address, 55 | api_url=api_url, 56 | log={ 57 | "level": "debug", 58 | "enable": True, 59 | }, 60 | ) 61 | ) 62 | 63 | 64 | async def cleanup(permit: Permit, project_key: str): 65 | for env in CREATED_ENVIRONMENTS: 66 | try: 67 | await permit.api.environments.delete(project_key, env.key) 68 | except PermitApiError as error: 69 | if error.status_code == 404: 70 | print(f"SKIPPING delete, env does not exist: {env.key}, project_key={project_key}") # noqa: T201 71 | 72 | 73 | async def test_environment_creation_with_org_level_api_key( 74 | permit_with_org_level_api_key: Permit, 75 | ): 76 | permit = permit_with_org_level_api_key 77 | try: 78 | await permit.api._ensure_access_level(ApiKeyAccessLevel.ORGANIZATION_LEVEL_API_KEY) 79 | except PermitContextError: 80 | logger.warning("this test must run with an org level api key") 81 | return 82 | 83 | try: 84 | await cleanup(permit, CREATED_PROJECTS[0].key) 85 | projects: List[ProjectRead] = [] 86 | for project_data in CREATED_PROJECTS: 87 | print(f"trying to creating project: {project_data.key}") # noqa: T201 88 | try: 89 | project: ProjectRead = await permit.api.projects.create(project_data) 90 | except PermitApiError as error: 91 | if error.status_code == 409: 92 | print(f"SKIPPING create, project already exists: {project_data.key}") # noqa: T201 93 | project: ProjectRead = await permit.api.projects.get(project_key=project_data.key) 94 | assert project is not None 95 | assert project.key == project_data.key 96 | assert project.name == project_data.name 97 | assert project.description == project_data.description 98 | projects.append(project) 99 | 100 | # create environments 101 | for environment_data in CREATED_ENVIRONMENTS: 102 | print(f"creating environment: {environment_data.key}") # noqa: T201 103 | environment: EnvironmentRead = await permit.api.environments.create( 104 | project_key=project.key, environment_data=environment_data 105 | ) 106 | assert environment is not None 107 | assert environment.key == environment_data.key 108 | assert environment.name == environment_data.name 109 | assert environment.description == environment_data.description 110 | assert environment.project_id == projects[0].id 111 | 112 | # initial number of items 113 | environments = await permit.api.environments.list(project_key=projects[0].key) 114 | assert len(CREATED_ENVIRONMENTS) + 2 == len( 115 | environments 116 | ) # each project has 2 default `dev` and `prod` environments 117 | 118 | # create first item 119 | test_environment = await permit.api.environments.get(CREATED_PROJECTS[0].key, CREATED_ENVIRONMENTS[0].key) 120 | 121 | assert test_environment is not None 122 | assert test_environment.key == CREATED_ENVIRONMENTS[0].key 123 | assert test_environment.name == CREATED_ENVIRONMENTS[0].name 124 | assert test_environment.description == CREATED_ENVIRONMENTS[0].description 125 | except PermitApiError as error: 126 | handle_api_error(error, "Got API Error") 127 | except PermitConnectionError: 128 | raise 129 | except Exception as error: # noqa: BLE001 130 | logger.error(f"Got error: {error}") 131 | pytest.fail(f"Got error: {error}") 132 | finally: 133 | await cleanup(permit, CREATED_PROJECTS[0].key) 134 | 135 | 136 | async def test_environment_creation_with_project_level_api_key( 137 | permit_with_project_level_api_key: Permit, 138 | ): 139 | permit = permit_with_project_level_api_key 140 | try: 141 | await permit.api._ensure_access_level(ApiKeyAccessLevel.PROJECT_LEVEL_API_KEY) 142 | except PermitContextError: 143 | logger.warning("this test must run with a project level api key") 144 | return 145 | 146 | try: 147 | project = permit.config.api_context.project 148 | assert project is not None 149 | project_id = str(project) 150 | 151 | project = await permit.api.projects.get(project_id) 152 | assert str(project.id) == project_id 153 | 154 | await cleanup(permit, project.key) 155 | 156 | # create environments 157 | for environment_data in CREATED_ENVIRONMENTS: 158 | print(f"creating environment: {environment_data.key}") # noqa: T201 159 | environment: EnvironmentRead = await permit.api.environments.create( 160 | project_key=project.key, environment_data=environment_data 161 | ) 162 | assert environment is not None 163 | assert environment.key == environment_data.key 164 | assert environment.name == environment_data.name 165 | assert environment.description == environment_data.description 166 | assert environment.project_id == project.id 167 | 168 | # initial number of items 169 | environments = await permit.api.environments.list(project_key=project.key) 170 | actual_env_set = {env.key for env in environments} 171 | created_env_set = {env.key for env in CREATED_ENVIRONMENTS} 172 | assert len(actual_env_set.intersection(created_env_set)) == 2 173 | except PermitApiError as error: 174 | handle_api_error(error, "Got API Error") 175 | except PermitConnectionError: 176 | raise 177 | except Exception as error: # noqa: BLE001 178 | logger.error(f"Got error: {error}") 179 | pytest.fail(f"Got error: {error}") 180 | finally: 181 | await cleanup(permit, project.key) 182 | -------------------------------------------------------------------------------- /tests/endpoints/test_error_response.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from loguru import logger 3 | 4 | from permit import Permit 5 | from permit.exceptions import PermitApiError, PermitConnectionError 6 | 7 | 8 | async def test_api_error(permit: Permit): 9 | try: 10 | await permit.api.users.get("this_key_does_not_exists") 11 | except PermitApiError as error: 12 | err = ( 13 | f"Got error: status={error.status_code}, url={error.request_url}, method={error.response.method}, " 14 | f"details={error.details}, content-type={error.content_type}" 15 | ) 16 | logger.info(err) 17 | assert error.content_type == "application/json" 18 | except PermitConnectionError: 19 | raise 20 | except Exception as error: # noqa: BLE001 21 | logger.error(f"Got error: {error}") 22 | pytest.fail(f"Got error: {error}") 23 | -------------------------------------------------------------------------------- /tests/endpoints/test_resources.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | from loguru import logger 5 | 6 | from permit import ActionBlockEditable, Permit, ResourceCreate 7 | from permit.exceptions import PermitAlreadyExistsError, PermitApiError 8 | 9 | TEST_RESOURCE_DOC_KEY = f"documento-{uuid.uuid4()}" 10 | TEST_RESOURCE_FOLDER_KEY = f"folder-{uuid.uuid4()}" 11 | CREATED_RESOURCES = [TEST_RESOURCE_DOC_KEY, TEST_RESOURCE_FOLDER_KEY] 12 | 13 | 14 | @pytest.mark.xfail() 15 | async def test_resources(permit: Permit): 16 | logger.info("initial setup of objects") 17 | len_original = 0 18 | # initial number of items 19 | resources = await permit.api.resources.list() 20 | len_original = len(resources) 21 | 22 | # create first item 23 | try: 24 | test_resource = await permit.api.resources.create( 25 | ResourceCreate( 26 | key=TEST_RESOURCE_DOC_KEY, 27 | name=TEST_RESOURCE_DOC_KEY, 28 | urn="prn:gdrive:test", 29 | description="a resource", 30 | actions={ 31 | "create": ActionBlockEditable(), 32 | "read": ActionBlockEditable(), 33 | "update": ActionBlockEditable(), 34 | "delete": ActionBlockEditable(), 35 | }, 36 | ) 37 | ) 38 | except PermitAlreadyExistsError: 39 | logger.info("Resource already exists...") 40 | test_resource = await permit.api.resources.get(TEST_RESOURCE_DOC_KEY) 41 | 42 | assert test_resource is not None 43 | assert test_resource.key == TEST_RESOURCE_DOC_KEY 44 | assert test_resource.name == TEST_RESOURCE_DOC_KEY 45 | assert test_resource.description == "a resource" 46 | assert test_resource.urn == "prn:gdrive:test" 47 | assert test_resource.actions is not None 48 | assert len(test_resource.actions) == 4 49 | assert set(test_resource.actions.keys()) == {"create", "read", "update", "delete"} 50 | 51 | # increased number of items by 1 52 | resources = await permit.api.resources.list() 53 | assert len(resources) == len_original 54 | # can find new item in the new list 55 | assert len([r for r in resources if r.key == test_resource.key]) == 1 56 | 57 | # get non existing -> 404 58 | with pytest.raises(PermitApiError) as e: 59 | await permit.api.resources.get("nosuchresource") 60 | assert e.value.status_code == 404 61 | 62 | # create existing -> 409 63 | with pytest.raises(PermitApiError) as e: 64 | await permit.api.resources.create({"key": TEST_RESOURCE_DOC_KEY, "name": "document2", "actions": {}}) 65 | assert e.value.status_code == 409 66 | 67 | # create empty item 68 | empty = await permit.api.resources.create( 69 | { 70 | "key": TEST_RESOURCE_FOLDER_KEY, 71 | "name": TEST_RESOURCE_FOLDER_KEY, 72 | "description": "empty resource", 73 | "actions": {}, 74 | } 75 | ) 76 | 77 | assert empty is not None 78 | assert empty.key == TEST_RESOURCE_FOLDER_KEY 79 | assert empty.name == TEST_RESOURCE_FOLDER_KEY 80 | assert empty.description == "empty resource" 81 | assert empty.actions is not None 82 | assert len(empty.actions) == 0 83 | 84 | resources = await permit.api.resources.list() 85 | assert len(resources) == len_original + 2 86 | 87 | # update actions 88 | await permit.api.resources.update( 89 | TEST_RESOURCE_FOLDER_KEY, 90 | {"description": "wat", "actions": {"pick": {}}}, 91 | ) 92 | 93 | # get 94 | new_empty = await permit.api.resources.get(TEST_RESOURCE_FOLDER_KEY) 95 | 96 | # new_empty changed 97 | assert new_empty is not None 98 | assert new_empty.key == TEST_RESOURCE_FOLDER_KEY 99 | assert new_empty.name == TEST_RESOURCE_FOLDER_KEY 100 | assert new_empty.description == "wat" 101 | assert new_empty.actions is not None 102 | assert len(new_empty.actions) == 1 103 | assert new_empty.actions.get("pick") is not None 104 | -------------------------------------------------------------------------------- /tests/endpoints/test_resources_sync.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | from loguru import logger 5 | 6 | from permit.exceptions import PermitApiError 7 | from permit.sync import Permit as SyncPermit 8 | 9 | TEST_RESOURCE_DOC_KEY = f"documento-{uuid.uuid4()}" 10 | TEST_RESOURCE_FOLDER_KEY = f"folder-{uuid.uuid4()}" 11 | CREATED_RESOURCES = [TEST_RESOURCE_DOC_KEY, TEST_RESOURCE_FOLDER_KEY] 12 | 13 | 14 | @pytest.mark.xfail() 15 | def test_resources_sync(sync_permit: SyncPermit): 16 | permit = sync_permit 17 | logger.info("initial setup of objects") 18 | len_original = 0 19 | # initial number of items 20 | resources = permit.api.resources.list() 21 | len_original = len(resources) 22 | 23 | # create first item 24 | try: 25 | test_resource = permit.api.resources.create( 26 | { 27 | "key": TEST_RESOURCE_DOC_KEY, 28 | "name": TEST_RESOURCE_DOC_KEY, 29 | "urn": "prn:gdrive:test", 30 | "description": "a resource", 31 | "actions": { 32 | "create": {}, 33 | "read": {}, 34 | "update": {}, 35 | "delete": {}, 36 | }, 37 | } 38 | ) 39 | except PermitApiError: 40 | logger.info("Resource already exists...") 41 | test_resource = permit.api.resources.get(TEST_RESOURCE_DOC_KEY) 42 | 43 | assert test_resource is not None 44 | assert test_resource.key == TEST_RESOURCE_DOC_KEY 45 | assert test_resource.name == TEST_RESOURCE_DOC_KEY 46 | assert test_resource.description == "a resource" 47 | assert test_resource.urn == "prn:gdrive:test" 48 | assert test_resource.actions is not None 49 | assert len(test_resource.actions) == 4 50 | assert set(test_resource.actions.keys()) == {"create", "read", "update", "delete"} 51 | 52 | # increased number of items by 1 53 | resources = permit.api.resources.list() 54 | assert len(resources) == len_original 55 | # can find new item in the new list 56 | assert len([r for r in resources if r.key == test_resource.key]) == 1 57 | 58 | # get non existing -> 404 59 | with pytest.raises(PermitApiError) as e: 60 | permit.api.resources.get("nosuchresource") 61 | assert e.value.status_code == 404 62 | 63 | # create existing -> 409 64 | with pytest.raises(PermitApiError) as e: 65 | permit.api.resources.create({"key": TEST_RESOURCE_DOC_KEY, "name": "document2", "actions": {}}) 66 | assert e.value.status_code == 409 67 | 68 | # create empty item 69 | empty = permit.api.resources.create( 70 | { 71 | "key": TEST_RESOURCE_FOLDER_KEY, 72 | "name": TEST_RESOURCE_FOLDER_KEY, 73 | "description": "empty resource", 74 | "actions": {}, 75 | } 76 | ) 77 | 78 | assert empty is not None 79 | assert empty.key == TEST_RESOURCE_FOLDER_KEY 80 | assert empty.name == TEST_RESOURCE_FOLDER_KEY 81 | assert empty.description == "empty resource" 82 | assert empty.actions is not None 83 | assert len(empty.actions) == 0 84 | 85 | resources = permit.api.resources.list() 86 | assert len(resources) == len_original + 2 87 | 88 | # update actions 89 | permit.api.resources.update( 90 | TEST_RESOURCE_FOLDER_KEY, 91 | {"description": "wat", "actions": {"pick": {}}}, 92 | ) 93 | 94 | # get 95 | new_empty = permit.api.resources.get_by_key(TEST_RESOURCE_FOLDER_KEY) 96 | 97 | # new_empty changed 98 | assert new_empty is not None 99 | assert new_empty.key == TEST_RESOURCE_FOLDER_KEY 100 | assert new_empty.name == TEST_RESOURCE_FOLDER_KEY 101 | assert new_empty.description == "wat" 102 | assert new_empty.actions is not None 103 | assert len(new_empty.actions) == 1 104 | assert new_empty.actions.get("pick") is not None 105 | -------------------------------------------------------------------------------- /tests/endpoints/test_role_assignments.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from permit import Permit, PermitApiError, RoleAssignmentCreate, RoleCreate, UserCreate 4 | 5 | 6 | @contextmanager 7 | def suppress_409(): 8 | try: 9 | yield 10 | except PermitApiError as e: 11 | if e.status_code != 409: 12 | raise e 13 | 14 | 15 | async def create_role_assignments(permit: Permit, role_key: str, user_count: int = 10): 16 | with suppress_409(): 17 | await permit.api.roles.create(RoleCreate(key=role_key, name=role_key)) 18 | with suppress_409(): 19 | await permit.api.users.bulk_create([UserCreate(key=f"user-{index}") for index in range(user_count)]) 20 | with suppress_409(): 21 | await permit.api.role_assignments.bulk_assign( 22 | [RoleAssignmentCreate(role=role_key, user=f"user-{index}", tenant="default") for index in range(user_count)] 23 | ) 24 | 25 | 26 | async def test_list_filter_by_role(permit: Permit): 27 | await create_role_assignments(permit, "role-1") 28 | await create_role_assignments(permit, "role-2") 29 | role_assignments = await permit.api.role_assignments.list(role_key="role-1") 30 | assert len(role_assignments) == 10 31 | assert {ra.role for ra in role_assignments} == {"role-1"} 32 | 33 | 34 | async def test_list_filter_by_role_multiple(permit: Permit): 35 | await create_role_assignments(permit, "role-1") 36 | await create_role_assignments(permit, "role-2") 37 | await create_role_assignments(permit, "role-3") 38 | role_assignments = await permit.api.role_assignments.list(role_key=["role-1", "role-2"]) 39 | assert len(role_assignments) == 20 40 | assert {ra.role for ra in role_assignments} == {"role-1", "role-2"} 41 | -------------------------------------------------------------------------------- /tests/endpoints/test_roles.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | from loguru import logger 5 | 6 | from permit import ActionBlockEditable, Permit, ResourceCreate 7 | from permit.exceptions import PermitAlreadyExistsError, PermitApiError 8 | 9 | TEST_RESOURCE_KEY = f"test-resource-{uuid.uuid4()}" 10 | TEST_ADMIN_ROLE_KEY = "testadmin" 11 | TEST_EMPTY_ROLE_KEY = "emptyrole" 12 | CREATED_RESOURCES = [TEST_RESOURCE_KEY] 13 | CREATED_ROLES = [TEST_ADMIN_ROLE_KEY, TEST_EMPTY_ROLE_KEY] 14 | 15 | 16 | @pytest.mark.xfail() 17 | async def test_roles(permit: Permit): 18 | logger.info("initial setup of objects") 19 | len_roles_original = 0 20 | try: 21 | await permit.api.resources.create( 22 | ResourceCreate( 23 | key=TEST_RESOURCE_KEY, 24 | name=TEST_RESOURCE_KEY, 25 | urn="prn:gdrive:test", 26 | actions={ 27 | "create": ActionBlockEditable(), 28 | "read": ActionBlockEditable(), 29 | "update": ActionBlockEditable(), 30 | "delete": ActionBlockEditable(), 31 | }, 32 | ) 33 | ) 34 | except PermitAlreadyExistsError: 35 | logger.info("Resource already exists...") 36 | 37 | # initial number of roles 38 | roles = await permit.api.roles.list() 39 | len_roles_original = len(roles) 40 | 41 | # create admin role 42 | admin = await permit.api.roles.create( 43 | { 44 | "key": TEST_ADMIN_ROLE_KEY, 45 | "name": TEST_ADMIN_ROLE_KEY, 46 | "description": "a test role", 47 | "permissions": [ 48 | f"{TEST_RESOURCE_KEY}:create", 49 | f"{TEST_RESOURCE_KEY}:read", 50 | ], 51 | } 52 | ) 53 | 54 | assert admin is not None 55 | assert admin.key == TEST_ADMIN_ROLE_KEY 56 | assert admin.name == TEST_ADMIN_ROLE_KEY 57 | assert admin.description == "a test role" 58 | assert admin.permissions is not None 59 | assert f"{TEST_RESOURCE_KEY}:create" in admin.permissions 60 | assert f"{TEST_RESOURCE_KEY}:read" in admin.permissions 61 | 62 | # increased number of roles by 1 63 | roles = await permit.api.roles.list() 64 | assert len(roles) == len_roles_original + 1 65 | # can find new role in the new list 66 | assert len([r for r in roles if r.key == admin.key]) == 1 67 | 68 | # get non existing role -> 404 69 | with pytest.raises(PermitApiError) as e: 70 | await permit.api.roles.get("nosuchrole") 71 | assert e.value.status_code == 404 72 | 73 | # create existing role -> 409 74 | with pytest.raises(PermitApiError) as e: 75 | await permit.api.roles.create( 76 | { 77 | "key": TEST_ADMIN_ROLE_KEY, 78 | "name": "TestAdmin2", 79 | } 80 | ) 81 | assert e.value.status_code == 409 82 | 83 | # create empty role 84 | empty = await permit.api.roles.create( 85 | { 86 | "key": TEST_EMPTY_ROLE_KEY, 87 | "name": TEST_EMPTY_ROLE_KEY, 88 | "description": "empty role", 89 | } 90 | ) 91 | 92 | assert empty is not None 93 | assert empty.key == TEST_EMPTY_ROLE_KEY 94 | assert empty.name == TEST_EMPTY_ROLE_KEY 95 | assert empty.description == "empty role" 96 | assert empty.permissions is not None 97 | assert len(empty.permissions) == 0 98 | 99 | roles = await permit.api.roles.list() 100 | assert len(roles) == len_roles_original + 2 101 | 102 | # assign permissions to roles 103 | assigned_empty = await permit.api.roles.assign_permissions(TEST_EMPTY_ROLE_KEY, [f"{TEST_RESOURCE_KEY}:delete"]) 104 | 105 | assert assigned_empty.key == empty.key 106 | assert len(assigned_empty.permissions) == 1 107 | assert f"{TEST_RESOURCE_KEY}:delete" in assigned_empty.permissions 108 | 109 | # remove permissions from role 110 | await permit.api.roles.remove_permissions(TEST_ADMIN_ROLE_KEY, [f"{TEST_RESOURCE_KEY}:create"]) 111 | 112 | # get 113 | admin = await permit.api.roles.get(TEST_ADMIN_ROLE_KEY) 114 | 115 | # admin changed 116 | assert admin is not None 117 | assert admin.key == TEST_ADMIN_ROLE_KEY 118 | assert admin.description == "a test role" 119 | assert f"{TEST_RESOURCE_KEY}:create" not in admin.permissions 120 | assert f"{TEST_RESOURCE_KEY}:read" in admin.permissions 121 | 122 | # update 123 | await permit.api.roles.update( 124 | TEST_ADMIN_ROLE_KEY, 125 | {"description": "wat"}, 126 | ) 127 | 128 | # get 129 | admin = await permit.api.roles.get(TEST_ADMIN_ROLE_KEY) 130 | 131 | # admin changed 132 | assert admin is not None 133 | assert admin.key == TEST_ADMIN_ROLE_KEY 134 | assert admin.description == "wat" 135 | assert f"{TEST_RESOURCE_KEY}:create" not in admin.permissions 136 | assert f"{TEST_RESOURCE_KEY}:read" in admin.permissions 137 | -------------------------------------------------------------------------------- /tests/endpoints/test_users_tenants.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | from loguru import logger 5 | 6 | from permit import Permit, RoleCreate, TenantCreate, UserCreate 7 | from permit.api.models import RoleAssignmentCreate, RoleAssignmentRemove 8 | from permit.exceptions import PermitApiError 9 | 10 | USER_A = UserCreate( 11 | key=str(uuid.uuid4()), 12 | email="asaf@permit.io", 13 | first_name="Asaf", 14 | last_name="Cohen", 15 | attributes={"age": 35}, 16 | ) 17 | USER_B = UserCreate( 18 | key=str(uuid.uuid4()), 19 | email="john@permit.io", 20 | first_name="John", 21 | last_name="Doe", 22 | attributes={"age": 27}, 23 | ) 24 | USER_BB = UserCreate( 25 | key=USER_B.key, 26 | email="john@apple.com", 27 | first_name="John", 28 | last_name="Appleseed", 29 | attributes={"age": 27}, 30 | ) 31 | USER_C = UserCreate( 32 | key=str(uuid.uuid4()), 33 | email="jane@permit.io", 34 | first_name="Jane", 35 | last_name="Doe", 36 | attributes={"age": 25}, 37 | ) 38 | 39 | TENANT_1 = TenantCreate(key=str(uuid.uuid4()), name="Tenant 1") 40 | TENANT_2 = TenantCreate(key=str(uuid.uuid4()), name="Tenant 2") 41 | 42 | ADMIN = RoleCreate(key=f"admin-{uuid.uuid4()}", name="Admin") 43 | VIEWER = RoleCreate(key=f"viewer-{uuid.uuid4()}", name="Viewer") 44 | 45 | CREATED_USERS = [USER_A, USER_B, USER_C] 46 | CREATED_TENANTS = [TENANT_1, TENANT_2] 47 | CREATED_ROLES = [ADMIN, VIEWER] 48 | 49 | 50 | async def test_users_tenants(permit: Permit): 51 | logger.info("initial setup of objects") 52 | # initial number of tenants 53 | tenants = await permit.api.tenants.list() 54 | len_original_tenants = len(tenants) 55 | 56 | # initial number of role assignments 57 | role_assignments = await permit.api.role_assignments.list() 58 | len_original_role_assignments = len(role_assignments) 59 | 60 | # create tenants 61 | for tenant_data in CREATED_TENANTS: 62 | tenant = await permit.api.tenants.create(tenant_data) 63 | assert tenant is not None 64 | assert tenant.key == tenant_data.key 65 | assert tenant.name == tenant_data.name 66 | assert tenant.description is None 67 | 68 | # increased number of tenants by 1 69 | tenants = await permit.api.tenants.list() 70 | assert len(tenants) == len_original_tenants + len(CREATED_TENANTS) 71 | 72 | # get non existing tenant -> 404 73 | with pytest.raises(PermitApiError) as e: 74 | await permit.api.tenants.get("nosuchtenant") 75 | assert e.value.status_code == 404 76 | 77 | # create existing tenant -> 409 78 | with pytest.raises(PermitApiError) as e: 79 | await permit.api.tenants.create(TENANT_1) 80 | assert e.value.status_code == 409 81 | 82 | # get tenant 83 | t1 = await permit.api.tenants.get(TENANT_1.key) 84 | assert t1.key == TENANT_1.key 85 | 86 | # create users 87 | for user_data in CREATED_USERS: 88 | user = await permit.api.users.create(user_data) 89 | assert user is not None 90 | assert user.key == user_data.key 91 | assert user.email == user_data.email 92 | assert user.first_name == user_data.first_name 93 | assert user.last_name == user_data.last_name 94 | assert set(user.attributes.keys()) == set(user_data.attributes.keys()) 95 | 96 | # get non existing user -> 404 97 | with pytest.raises(PermitApiError) as e: 98 | await permit.api.users.get("nosuchuser") 99 | assert e.value.status_code == 404 100 | 101 | # create existing user -> 409 102 | with pytest.raises(PermitApiError) as e: 103 | await permit.api.users.create(USER_BB) 104 | assert e.value.status_code == 409 105 | 106 | # get user 107 | ub = await permit.api.users.get(USER_B.key) 108 | assert ub.key == USER_B.key 109 | assert ub.email == USER_B.email 110 | 111 | # but we can sync the user 112 | user = await permit.api.users.sync(USER_BB) 113 | assert user is not None 114 | assert user.key == USER_BB.key 115 | assert user.email == USER_BB.email 116 | assert user.first_name == USER_BB.first_name 117 | assert user.last_name == USER_BB.last_name 118 | assert set(user.attributes.keys()) == set(USER_BB.attributes.keys()) 119 | 120 | # get user after sync/update 121 | ub = await permit.api.users.get(USER_B.key) 122 | assert ub.key == USER_B.key 123 | assert ub.email != USER_B.email 124 | assert ub.email == USER_BB.email 125 | 126 | # update tenant 127 | t2 = await permit.api.tenants.update(TENANT_2.key, {"description": "t2 update"}) 128 | assert t2.key == TENANT_2.key 129 | assert t2.description != TENANT_2.description 130 | assert t2.description == "t2 update" 131 | 132 | # get tenant users 133 | users = await permit.api.tenants.list_tenant_users(TENANT_1.key) 134 | assert len(users.data) == 0 135 | 136 | # create roles 137 | for role_data in CREATED_ROLES: 138 | await permit.api.roles.create(role_data) 139 | 140 | # bulk role assignment to tenant 1 141 | await permit.api.role_assignments.bulk_assign( 142 | [ 143 | RoleAssignmentCreate(user=USER_A.key, role=ADMIN.key, tenant=TENANT_1.key), 144 | RoleAssignmentCreate(user=USER_B.key, role=VIEWER.key, tenant=TENANT_1.key), 145 | ] 146 | ) 147 | 148 | # get tenant users 149 | users1 = await permit.api.tenants.list_tenant_users(TENANT_1.key) 150 | assert len(users1.data) == 2 151 | users2 = await permit.api.tenants.list_tenant_users(TENANT_2.key) 152 | assert len(users2.data) == 0 153 | 154 | # get assigned roles of user A 155 | roles_a1 = await permit.api.users.get_assigned_roles(USER_A.key, tenant=TENANT_1.key) 156 | assert len(roles_a1) == 1 157 | assert roles_a1[0].user == USER_A.key 158 | assert roles_a1[0].role == ADMIN.key 159 | assert roles_a1[0].tenant == TENANT_1.key 160 | roles_a2 = await permit.api.users.get_assigned_roles(USER_A.key, tenant=TENANT_2.key) 161 | assert len(roles_a2) == 0 162 | 163 | # assign role 164 | ra = await permit.api.users.assign_role(RoleAssignmentCreate(user=USER_C.key, role=ADMIN.key, tenant=TENANT_2.key)) 165 | assert ra.user == USER_C.key or ra.user == USER_C.email # TODO: fix bug in api 166 | assert ra.role == ADMIN.key 167 | assert ra.tenant == TENANT_2.key 168 | 169 | # add user a to another tenant 170 | ra = await permit.api.users.assign_role(RoleAssignmentCreate(user=USER_A.key, role=ADMIN.key, tenant=TENANT_2.key)) 171 | 172 | # get assigned roles 173 | roles_a = await permit.api.users.get_assigned_roles(USER_A.key) 174 | assert len(roles_a) == 2 175 | assert len({ra.tenant for ra in roles_a}) == 2 # user in 2 tenants 176 | 177 | # delete tenant user 178 | tenant2_users = await permit.api.tenants.list_tenant_users(TENANT_2.key) 179 | assert len(tenant2_users.data) == 2 180 | await permit.api.tenants.delete_tenant_user(TENANT_2.key, USER_A.key) 181 | tenant2_users = await permit.api.tenants.list_tenant_users(TENANT_2.key) 182 | assert len(tenant2_users.data) == 2 # TODO: change to 1, fix bug in delete_tenant_user 183 | 184 | # list role assignments 185 | role_assignments = await permit.api.role_assignments.list() 186 | assert len(role_assignments) == len_original_role_assignments + 3 187 | 188 | # role unassign 189 | await permit.api.role_assignments.unassign( 190 | RoleAssignmentRemove(user=USER_C.key, role=ADMIN.key, tenant=TENANT_2.key) 191 | ) 192 | 193 | # list role assignments 194 | role_assignments = await permit.api.role_assignments.list() 195 | assert len(role_assignments) == len_original_role_assignments + 2 196 | 197 | # bulk unassign 198 | await permit.api.role_assignments.bulk_unassign( 199 | [ 200 | RoleAssignmentRemove(user=USER_A.key, role=ADMIN.key, tenant=TENANT_1.key), 201 | RoleAssignmentRemove(user=USER_B.key, role=VIEWER.key, tenant=TENANT_1.key), 202 | ] 203 | ) 204 | 205 | # list role assignments 206 | role_assignments = await permit.api.role_assignments.list() 207 | assert len(role_assignments) == len_original_role_assignments 208 | -------------------------------------------------------------------------------- /tests/test_abac_pdp.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | import aiohttp 4 | import pytest 5 | 6 | from permit import Permit, PermitConnectionError, TenantCreate, UserCreate 7 | 8 | 9 | def abac_user(user: UserCreate): 10 | return user.dict(exclude={"first_name", "last_name"}) 11 | 12 | 13 | async def test_abac_pdp_cloud_error(permit_cloud: Permit): 14 | user_test = UserCreate( 15 | key="maya@permit.io", 16 | email="maya@permit.io", 17 | first_name="Maya", 18 | last_name="Barak", 19 | attributes={"age": 23}, 20 | ) 21 | tesla = TenantCreate(key="tesla", name="Tesla Inc") 22 | 23 | try: 24 | await permit_cloud.check( 25 | abac_user(user_test), 26 | "sign", 27 | { 28 | "type": "document", 29 | "tenant": tesla.key, 30 | "attributes": {"private": False}, 31 | }, 32 | ) 33 | except (PermitConnectionError, aiohttp.ClientError) as error: 34 | assert isinstance(error, PermitConnectionError) 35 | else: 36 | pytest.fail("Should have raised an exception") 37 | 38 | 39 | async def test_get_user_permissions_cloud_error(permit_cloud: Permit): 40 | user_test = UserCreate( 41 | key="maya@permit.io", 42 | email="maya@permit.io", 43 | first_name="Maya", 44 | last_name="Barak", 45 | attributes={"age": 23}, 46 | ) 47 | 48 | try: 49 | await permit_cloud.get_user_permissions( 50 | user={"key": user_test.key, "email": user_test.email, "attributes": user_test.attributes}, 51 | tenants=["default"], 52 | resources=["Blog:dddddd"], 53 | resource_types=["Blog"], 54 | ) 55 | except (PermitConnectionError, aiohttp.ClientError) as error: 56 | assert isinstance(error, PermitConnectionError) 57 | else: 58 | pytest.fail("Should have raised an exception") 59 | 60 | 61 | async def test_filter_objects_cloud_error(permit_cloud: Permit): 62 | user_test = {"key": "maya@permit.io", "email": "maya@permit.io", "attributes": {"age": 23}} 63 | 64 | test_resources: List[Dict[str, Any]] = [ 65 | {"type": "Blog", "key": "doc1", "context": {}, "attributes": {}, "tenant": "default"}, 66 | {"type": "Document", "key": "doc2", "context": {}, "attributes": {}, "tenant": "default"}, 67 | ] 68 | 69 | try: 70 | await permit_cloud.filter_objects(user=user_test, action="read", context={}, resources=test_resources) 71 | except (PermitConnectionError, aiohttp.ClientError) as error: 72 | assert isinstance(error, PermitConnectionError) 73 | else: 74 | pytest.fail("Should have raised an exception") 75 | -------------------------------------------------------------------------------- /tests/test_sync_client.py: -------------------------------------------------------------------------------- 1 | import random 2 | from concurrent.futures.thread import ThreadPoolExecutor 3 | 4 | import pytest 5 | 6 | from permit import PermitConfig, UserCreate 7 | from permit.sync import Permit 8 | 9 | 10 | @pytest.fixture() 11 | def permit(permit_config: PermitConfig) -> Permit: 12 | return Permit(permit_config) 13 | 14 | 15 | def test_sync_client(permit: Permit): 16 | user_key = f"user-{random.randint(0, 1000)}" 17 | permit.api.users.create( 18 | UserCreate( 19 | key=user_key, 20 | email="test@example.com", 21 | ) 22 | ) 23 | user = permit.api.users.get(user_key) 24 | assert user.key == user_key 25 | permit.api.users.delete(user_key) 26 | 27 | 28 | def test_sync_client_multithreading(permit_config: PermitConfig): 29 | instances = [Permit(permit_config) for _ in range(10)] 30 | 31 | with ThreadPoolExecutor() as executor: 32 | for instance in instances: 33 | executor.submit(test_sync_client, instance) 34 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from loguru import logger 3 | 4 | from permit.exceptions import PermitApiError 5 | 6 | 7 | def handle_api_error(error: PermitApiError, message: str): 8 | err = ( 9 | f"{message}: status={error.status_code}, url={error.request_url}, method={error.response.method}, " 10 | f"details={error.details}, content-type={error.content_type}" 11 | ) 12 | logger.error(err) 13 | pytest.fail(err) 14 | --------------------------------------------------------------------------------