Skip to main content

Manage policy and check permissions with the Python SDK

This page collects short Python SDK examples for the most common calls: create a Permit client and turn on its logs, manage resources, roles, tenants, and users, assign and list roles, handle API errors, check permissions, and refresh the data of your PDPs. It also lists the deprecated APIs that permit 4.0 removes. This page is for backend developers who have installed the permit package and want the call and argument shape for each task. To manage groups, see Manage groups with the Python SDK. To install the SDK and run a first check, see Check permissions with the Python SDK (asyncio).

The Permit class from from permit import Permit is asynchronous. Every method returns a coroutine, so run the examples with await inside an async function. For code that doesn't use asyncio, import Permit from permit.sync instead and call the same methods without await. See Check permissions with the Python SDK (sync).

Prerequisites​

Create a Permit client​

Create one Permit client and reuse it across your application.

ArgumentDescription
tokenAPI key of the Permit environment. The API key selects the project and environment of every call.
pdpAddress of the PDP that permit.check() calls, for example http://localhost:7766 for a PDP container.
logOptional. SDK log settings: enable, level, and label. See Turn on SDK logs.
from permit import Permit

permit = Permit(
# the API key to the Permit environment you wish to connect to
token="<YOUR_API_KEY>",
# the url in which the SDK can connect to the PDP container
pdp="http://localhost:7766",
)

Turn on SDK logs​

The SDK logs with loguru, and logs nothing unless you turn logs on with the log option. This section describes the log option of permit 3.1 or later.

from permit import Permit

# permit 3.1 or later
permit = Permit(
token="<YOUR_API_KEY>",
pdp="http://localhost:7766",
log={"enable": True, "level": "debug", "label": "Permit"},
)
SettingDefaultDescription
enableFalseWhether the SDK logs. False calls loguru's logger.disable("permit"). True undoes that call only if an earlier client made it, so a logger.disable() that your application made for permit still applies.
level"info"The lowest severity the SDK logs, such as "debug", "info", "warning", or "error". The SDK drops its records below this level before they reach a sink, and your application's own records aren't affected. For a level name that loguru doesn't know, the SDK logs a warning that names the level, and logs at "info".
label"Permit"The text the SDK puts in square brackets before every message it logs, as in [Permit] .... An empty string adds nothing.
jsonFalseNot applied. For JSON output, see the loguru sink below.
  • The SDK adds no loguru sink of its own. Its records go to the sinks your application added, or to loguru's default stderr sink, in the format of those sinks.
  • The SDK logs its HTTP requests and the PDP's responses at the "debug" level.
  • loguru's logger is process-wide, so the log settings are too. The client created last decides whether the SDK logs, and the last client created with "enable": True decides the level and the label, for every client in the process. wait_for_sync() doesn't create a client, so it doesn't change the settings.

For JSON output, replace loguru's default sink with a serialized sink. A serialized sink that you add next to the default sink prints every record a second time.

import sys

from loguru import logger

logger.remove()
logger.add(sys.stderr, serialize=True)

Close the client's connections​

In permit 3.1 or later, a client keeps the HTTP connections it opens and reuses them for its next requests, so a request doesn't open a new connection, with a new TLS handshake, each time. The client keeps one pool of connections for the Permit API and one for the PDP, and opens each pool on its first request. The asyncio client keeps these pools per event loop it's used on.

Closing the client is optional. For a client that lives as long as your application, you don't need to close it. For a client that does one task, such as a script, close it when the task ends: use the client as a context manager, or call close(). The context manager and close() need permit 3.1 or later.

import asyncio

from permit import Permit


async def main():
# permit 3.1 or later
async with Permit(token="<YOUR_API_KEY>", pdp="http://localhost:7766") as permit:
allowed = await permit.check("auth0|user-1", "read", "document")
print(allowed)


asyncio.run(main())

With the blocking client, use with Permit(...) as permit: and call permit.close() without await.

  • await permit.close() on the asyncio client, or permit.close() on the blocking client, closes the connections, as leaving the with block does. A second close() closes nothing more. The client stays usable: its next request opens new connections.
  • An asyncio client that you never close leaves nothing open when its event loop shuts down through asyncio.run() or asyncio.Runner. If you run an event loop yourself, run await permit.close() on that loop before you close it. Connections left on a closed loop stay open until the client's next request lets the garbage collector free them, and Python reports each one with a ResourceWarning.
  • Close a client once no request is in flight. A request in flight when close() runs fails.
  • The blocking client, permit.sync.Permit, runs its calls on an event loop in a background daemon thread, which it starts on its first call. Its close() waits for the calls that other threads have in flight, then closes the connections and stops the thread. Don't call the blocking client from code that runs on that thread, such as a callback scheduled on its loop: the call raises RuntimeError.
  • With proxy_facts_via_pdp on, the client that wait_for_sync() yields uses the connections of the client you call it on. Close the client you called wait_for_sync() on. The yielded client's close() does nothing.

Manage resources​

A resource is a type of object in your application, such as document, with the actions that policies grant on it.

Create a resource​

permit.api.resources.create() takes a resource object and returns a ResourceRead object. The key and name fields and the actions dictionary are required. Each key of actions is an action key. The attributes dictionary defines resource attributes for attribute-based access control (ABAC) policies.

from permit import ResourceRead

document: ResourceRead = await permit.api.resources.create(
{
"key": "document",
"name": "Document",
"urn": "prn:gdrive:document",
"description": "google drive document",
"actions": {
"create": {},
"read": {},
"update": {},
"delete": {},
},
"attributes": {
"private": {
"type": "bool",
"description": "whether the document is private",
},
},
}
)

Update a resource​

permit.api.resources.update() takes the resource key and an object with the fields to change, and returns the updated ResourceRead object. The SDK sends a PATCH request, so fields that you omit keep their values. The example changes the description of the document resource and sets its actions.

from permit import ResourceRead

resource_after_changes: ResourceRead = await permit.api.resources.update(
# the key of the resource
"document",
# updated fields
{
"description": "a document in the document store",
"actions": {
"find": {}
}
},
)

List resources​

permit.api.resources.list() returns a list of ResourceRead objects. The method takes optional page and per_page arguments. per_page defaults to 100.

from typing import List

from permit import ResourceRead

resources: List[ResourceRead] = await permit.api.resources.list()

Get a resource​

permit.api.resources.get() takes a resource key and returns a ResourceRead object. The example gets the resource with the key document:

from permit import ResourceRead

resource: ResourceRead = await permit.api.resources.get("document")

Handle API errors​

If the Permit API returns an error status code, the SDK raises a PermitApiError. Read the HTTP status code from status_code. A missing object returns status code 404, and a create call with an existing key returns status code 409. The SDK also raises the subclasses PermitNotFoundError for 404 and PermitAlreadyExistsError for 409, which you can catch directly.

from permit import Permit, PermitApiError

permit = Permit(token="<YOUR_API_KEY>", pdp="http://localhost:7766")

# handle not found error
try:
await permit.api.resources.get("nosuchresource")
except PermitApiError as e:
if e.status_code == 404:
print("not found")
else:
...

# handle cannot create object due to key conflict:
try:
await permit.api.resources.create(
{"key": "document", "name": "document2", "actions": {}}
)
except PermitApiError as e:
if e.status_code == 409:
print("already exists!")
else:
...

Manage roles​

Create a role​

permit.api.roles.create() takes a role object and returns a RoleRead object. Each permission uses the format resource:action, for example document:read. Create the resource before the role.

from permit import RoleRead

admin: RoleRead = await permit.api.roles.create(
{
"key": "admin",
"name": "Admin",
"description": "an admin role",
"permissions": ["document:create", "document:read"],
}
)

Manage tenants​

Create a tenant​

permit.api.tenants.create() takes a tenant object with a key and a name, and returns a TenantRead object. A tenant is an isolated group of users and resources, such as one customer organization.

from permit import TenantRead

tenant: TenantRead = await permit.api.tenants.create(
{
"key": "acme",
"name": "Acme Inc",
"description": "An example customer",
}
)

Create a user as a member of a tenant​

permit.api.tenants.create_user() takes a tenant key and a user object, creates the user as a member of that tenant with no role there, and returns a UserRead object. It needs permit 3.1 or later.

from permit import UserRead

user: UserRead = await permit.api.tenants.create_user(
# the tenant key
"acme",
# the new user
{"key": "auth0|user-2", "email": "user2@example.com"},
)
  • The user must be new. When a user with the same key exists, in any tenant, the Permit API returns status code 409 and the SDK raises PermitAlreadyExistsError. To give an existing user access to another tenant, assign the user a role in that tenant with permit.api.users.assign_role().
  • The Permit API grants the role assignments in the user object's role_assignments field as it does for permit.api.users.create(), each in the tenant it names.
  • The request always goes to the Permit API, even with proxy_facts_via_pdp on, so it doesn't wait for the PDP to have the user.
  • permit.api.tenants.delete_tenant_user() returns status code 404 for a member with no role in the tenant. To remove such a member, delete the user with permit.api.users.delete().
  • permit.api.tenants.add_user() is a deprecated alias of create_user().

Sync users​

Create or update a user (sync user)​

permit.api.users.sync() creates the user if the key doesn't exist in the environment, or replaces the user data if the key exists. The method returns a UserRead object. The key field is required. Pass the same key to permit.check(). For when and how to sync users, see Sync users.

from permit import UserRead

user: UserRead = await permit.api.users.sync(
{
"key": "auth0|user-1",
"email": "user1@example.com",
"first_name": "Ada",
"last_name": "Example",
"attributes": {
"age": 50,
"favorite_color": "red",
},
}
)

Assign and list roles​

Assign a role to a user in a tenant​

permit.api.users.assign_role() takes an object with the user, role, and tenant keys, and returns a RoleAssignmentRead object. Sync the user and create the role and the tenant before you assign the role.

ra = await permit.api.users.assign_role(
{
# the user key
"user": "auth0|user-1",
# the role key
"role": "viewer",
# the tenant key
"tenant": "acme",
}
)

List role assignments​

permit.api.role_assignments.list() returns a list of RoleAssignmentRead objects. Filter the list with the keyword arguments user_key, role_key, tenant_key, resource_key, and resource_instance_key. The user_key, role_key, and tenant_key arguments accept one key or a list of keys. The method also takes page and per_page (default 100). For more filters, see List role assignments.

assignments = await permit.api.role_assignments.list(
tenant_key="acme",
role_key="viewer",
)

To filter by more than one role, pass a list:

assignments = await permit.api.role_assignments.list(
tenant_key="acme",
role_key=["viewer", "editor"],
)

List role assignments with their details​

permit.api.role_assignments.list_detailed() takes the filters of list() as keyword arguments, and returns one page of role assignments with the total count. Where list() returns the keys of the role, the user, and the tenant, each detailed role assignment has them as objects with their names and attributes, and also has the resource instance of a resource role. It needs permit 3.1 or later.

page = await permit.api.role_assignments.list_detailed(
user_key="auth0|user-1",
tenant_key="acme",
)
for assignment in page.data:
print(assignment.role.name, assignment.tenant.name, assignment.user.email)
print(page.total_count)

permit.api.resource_instances and permit.api.relationship_tuples have a list_detailed() method too, with the filters of their list() methods:

MethodWhat each result has beyond list()
permit.api.role_assignments.list_detailed()The role, the user, the tenant, and for a resource role the resource instance, as objects. per_page is up to 1000.
permit.api.resource_instances.list_detailed()relationships: the relationship tuples whose subject or object the instance is. search_key matches an instance key or ID exactly, where list() also matches part of a key.
permit.api.relationship_tuples.list_detailed()subject_details, relation_details, object_details, and tenant_details, which list() leaves empty.
  • Each method needs an environment API key, or a project or organization API key with the SDK's API context set to the environment.
  • With proxy_facts_via_pdp on, the requests go through the PDP, which forwards only the last value of a filter given as a list: user_key=["alice", "bob"] lists only the role assignments of bob. Pass lists only with proxy_facts_via_pdp off.
  • permit.api.resource_instances.list(detailed_key=True) is deprecated. Use permit.api.resource_instances.list_detailed() instead.

List the tenants a user has a role in​

permit.get_user_tenants() asks the PDP for the tenants in which a user has a tenant-level role, the kind permit.api.users.assign_role() grants. It returns a list of TenantDetails objects, each with the tenant's key and attributes. It needs permit 3.1 or later and a container PDP.

tenants = await permit.get_user_tenants("auth0|user-1")
print([tenant.key for tenant in tenants])

After the role assignment in Assign a role to a user in a tenant, the list holds the acme tenant once the PDP has the assignment.

  • A role on a resource instance doesn't count, and neither does membership without a role, such as permit.api.tenants.create_user() creates.
  • The list is empty when the user has no tenant-level role, or the PDP doesn't know the user.
  • The Cloud PDP doesn't serve this query. It returns status code 404, and the SDK raises a PermitConnectionError that says the query needs a container PDP.

Check permissions​

permit.check() takes a user, an action, and a resource, sends the check to the PDP, and returns True or False. Pass the user as a key string, and the resource as a resource key string or a dictionary with type and tenant. The code comments in the example list the conditions that a role-based access control (RBAC) policy needs to return True.

from permit import Permit

permit = Permit(token="<YOUR_API_KEY>", pdp="http://localhost:7766")

# in order to be permitted according to the RBAC policy, a few conditions must be met:
# 1) the user must exist in the permit system (you called sync user before)
# 2) the checked resource belongs to tenant X
# 3) the user has an assigned role in tenant X (the user must have at
# least one assigned role in the tenant that contains the resource)
# 4) the role assigned to the user must have the permission to perform
# the checked action on the checked resource
permitted = await permit.check(
# the user key
"auth0|user-1",
# the action
"create",
# the resource
{
# the type of the resource (resource.key)
"type": "document",
# the tenant that contains the resource
"tenant": "acme"
},
)

if permitted:
print("permitted")
else:
print("denied")

To confirm the result, open the audit log in the Permit dashboard. The check appears with the user, action, resource, and decision.

Refresh the data of the environment's PDPs​

permit.api.pdps.refresh() makes every PDP connected to the environment fetch all of its authorization data from Permit again, instead of at its next periodic update. Use it when the data the PDPs decide on changed outside Permit, such as in an external data source. It needs permit 3.1 or later.

refreshed = await permit.api.pdps.refresh(reason="nightly import")
print(refreshed.update_id, refreshed.pdp_ids)
  • refresh() returns once Permit has triggered the refresh, not once the PDPs have finished it. A check sent right after refresh() returns can still get an answer from the old data.
  • reason is optional, at most 512 characters, and shows in the PDPs' logs. With a longer reason, the SDK raises a validation error and sends nothing.
  • The result has the update_id of the data update that carries the refresh, and the pdp_ids of the PDP configurations it was sent to.
  • refresh() needs an environment API key with write or admin access, or a project or organization API key with the SDK's API context set to the environment. The Permit API returns status code 403 for a read-only key, and 404 for an environment with no PDP configuration.

Deprecated APIs​

permit 4.0, a future major release, removes these APIs. Each one issues a DeprecationWarning that says what to use instead. tenants.add_user() and its replacement, create_user(), need permit 3.1 or later, and so do the detailed_key warning and the PermitException warning that this section describes.

DeprecatedUse instead
pydantic 1 support. On pydantic 1, import permit warns once.pydantic 2. The SDK's models then come from pydantic.v1, so invalid input raises pydantic.v1.ValidationError, which you can catch under both pydantic versions.
The flat methods on permit.api, such as permit.api.get_user() and permit.api.sync_user().The grouped APIs, such as permit.api.users.get() and permit.api.users.sync(). Each warning names the replacement.
permit.api.tenants.add_user().permit.api.tenants.create_user(), which add_user() calls.
The detailed_key argument of permit.api.resource_instances.list(). Only a call that passes detailed_key=True or detailed_key=False warns.permit.api.resource_instances.list_detailed(). See List role assignments with their details.
PermitException.Catch PermitConnectionError, the only exception the SDK raises that is a PermitException. In permit 4.0, PermitConnectionError becomes a direct subclass of PermitError, so handlers of either class keep working.

In permit 3.1 or later, PermitException warns only at the line that imports it, or that reads permit.PermitException or permit.exceptions.PermitException. A star import of permit or permit.exceptions binds PermitException, so it warns at the star import line. To avoid that warning, import the names your code uses. permit 3.0 warns once instead, when import permit runs, with the message "Use PermitError instead".

Python shows a DeprecationWarning only when the code that triggers the warning is in __main__, such as the script you run. pytest lists the warnings in its warnings summary. To see the warnings elsewhere, such as in a web app, run Python with -W default::DeprecationWarning, or set the environment variable PYTHONWARNINGS=default::DeprecationWarning. For warning filters that silence the pydantic 1 and flat method warnings while you migrate, see the Python SDK 3.0 migration guide. In permit 3.1 or later, the filter ignore:PermitException is deprecated:DeprecationWarning silences the PermitException warning. In permit 3.0, the filter ignore:Use PermitError instead:DeprecationWarning does.

Next steps​