What You’ll Build
Three users, two documents, and one instance with access control on.user_1 remembers a PDF into an AI dataset while user_2 remembers a text passage into a QUANTUM dataset, and the script then works through what user_1 can and cannot do with someone else’s dataset: reads and writes are refused, a read grant from the owner turns the refusal into an answer, and a CogneeLab tenant with a Researcher role finally lets user_3 read a tenant-owned dataset on the strength of role membership alone. Every refusal along the way is a real PermissionDeniedError caught and printed, so a run reads as a transcript of the permission system making decisions.
The complete runnable script is
examples/demos/permissions/user_permissions_and_access_control_example.py —
this page walks through its key moments rather than reproducing it.
Features in Play
- Permission Snippets — the tenant and role calls in short copy-paste form, with the four permission names spelled out; it grants with the unchecked
give_permission_on_datasetrather than the authorizing wrapper used here - ACL —
authorized_give_permission_on_datasetswrites the rows that decide each read in this demo, and raisesPermissionDeniedErrorwhen there is no row to match - Tenants — the
CogneeLaborganization, and the active-tenant context that decides which datasets a grant may target - Roles — the
Researcherrole, so access is granted once and every member inherits it - Remember — each ingestion call runs as a
userand makes that user the sole owner of the dataset it creates - Recall — every read names both a
userand explicitdataset_ids, and returns results only when an ACL allows it
What to Expect
The excerpts below come from one real run, trimmed of most log lines. Every ingestion and every recall is a live LLM call, so the wording of the answers varies from run to run; the permission decisions do not. The instance is emptied and each owner is registered in turn.Data reset complete. follows the two prunes, Relational migrations applied (target head). is setup() rebuilding the user-management tables, and each create_user call prints the address it registers before the new principal’s id is logged.
user_1 recalls from ai_dataset_id and a graph_completion result comes straight back, summarizing the PDF it ingested a moment earlier — the owner holds every permission on the dataset its remember() created.
user_2’s dataset raises PermissionDeniedError, and so does a remember() into it. The 403 lines are cognee logging the exception before the script catches it and prints its own line, so expect them on a healthy run — this is what ownership alone gets everyone else: nothing.
"read" grant turns the refusal into a result. The recall that failed two steps earlier is replayed unchanged, and this time recall: 1 results is logged and a completion is returned instead of a 403. The ACL row is the only thing that changed between the two attempts.
user_3 selecting CogneeLab as its own active tenant. Read the last two lines closely — user_2 owns both CogneeLab and the QUANTUM dataset, but that dataset belongs to user_2 personally, so it is out of scope for a grant made from inside the tenant.
user_3 reads it. No grant anywhere in the run names user_3: the final read succeeds purely through membership of Researcher, which is the reason to grant at the role level at all.
Before You Start
- Complete Quickstart to understand basic operations
- Ensure you have LLM Providers configured — the ingestion calls and every
GRAPH_COMPLETIONrecall in the script make live LLM calls - Leave multi-user mode on: the whole demo depends on
ENABLE_BACKEND_ACCESS_CONTROL, which is enabled by default, and on graph and vector backends that support per-dataset isolation — see Permissions Setup and Multi-User Mode Overview - Run it from a checkout of the cognee repo: the script reads
artificial_intelligence.pdffrom the siblingdata/folder next to it - Point it at a scratch instance: it opens with
prune_data()andprune_system(metadata=True), and the second of those drops the relational database — users, tenants, and ACLs included — rather than deleting one dataset
How It Works
Stage 1: Create the User-Management Tables
setup() recreates the relational tables the permission system lives in: principals, tenants, roles, and the ACL rows that join them to datasets. Nothing later in the script — not even creating the first user — works without it.
Stage 2: Give Each Owner Their Own Dataset
user=user_1 is what makes this ingestion belong to someone: the AI dataset is created under user_1, who is initially the only principal with any permission on it. user_2 is created the same way a few lines down and remembers an inline passage about quantum computing into a QUANTUM dataset, which gives the rest of the script two datasets with two different owners to negotiate over.
Stage 3: Address Datasets by ID, Not Name
remember() result carries the id of the dataset it wrote to, and those ids are how the script refers to datasets from here on. The comment names the reason: a dataset name is only ever resolved among the datasets the calling user owns, so dataset_name="QUANTUM" as user_1 would look for a second, different dataset instead of reaching user_2’s. Ids are used for the owner’s own data too: the first recall passes dataset_ids=[ai_dataset_id] and returns an answer immediately.
Stage 4: Watch the Default Denial
user_1’s own dataset raises PermissionDeniedError when it points at user_2’s, because ownership grants nothing to anyone else. Writes are refused on the same grounds: the block that follows sends a remember() at dataset_id=quantum_dataset_id as user_1 and catches the identical error, since user_1 holds neither read nor write on that dataset.
Stage 5: Grant One User Read Access
"read" row later, the recall from Stage 4 is replayed unchanged and returns an answer. Only reads are open: remembering into QUANTUM as user_1 would still fail, because "write" was never granted.
Stage 6: Set Up a Tenant and a Role
user_2 builds an organization instead: a CogneeLab tenant, selected as the active tenant, and a Researcher role inside it. The order is forced by the model — a role can only be created by a tenant owner, and a user can only join a role once they are in the same tenant, which is why user_3 is added to CogneeLab before being added to Researcher. user_3 then selects CogneeLab as its own active tenant.
Stage 7: Hit the Tenant-Scoping Constraint
QUANTUM dataset fails, and this refusal is the one worth understanding: user_2 owns that dataset and owns the tenant, but the dataset was created before CogneeLab existed and therefore belongs to user_2 personally. A grant is evaluated inside the acting user’s active tenant, and a personal dataset is out of that scope — so the answer is the same PermissionDeniedError as Stage 4, for a completely different reason.
Stage 8: Own the Dataset in the Tenant, Then Grant to the Role
user_2 is re-read from the database so the in-memory object carries the new active tenant, and the same passage is remembered again as QUANTUM_COGNEE_LAB — a distinct dataset with its own id, this time owned inside CogneeLab. The personal QUANTUM dataset is untouched and still reachable by selecting user_2’s personal tenant. Granting "read" to role_id now succeeds, and the closing recall proves the payoff: user_3, who was never named in any grant, reads the tenant dataset as a member of Researcher.
Run It
Permission Snippets
Every call from this walkthrough as a standalone snippet, plus the permission names.
Permissions System Overview
How principals, datasets, and ACLs fit together behind these calls.
Multi-User Mode Overview
What access control changes about storage and isolation across a deployment.
Permissions Setup
The environment variables and backend support the demo assumes.