Skip to main content
Two people ingest documents into the same cognee deployment, and neither should see the other’s data by default — until one of them asks for access, and later a whole research team needs it. That is the sequence every multi-user deployment runs into the first time someone says “can you share that dataset with me?”

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_dataset rather than the authorizing wrapper used here
  • ACLauthorized_give_permission_on_datasets writes the rows that decide each read in this demo, and raises PermissionDeniedError when there is no row to match
  • Tenants — the CogneeLab organization, and the active-tenant context that decides which datasets a grant may target
  • Roles — the Researcher role, so access is granted once and every member inherits it
  • Remember — each ingestion call runs as a user and makes that user the sole owner of the dataset it creates
  • Recall — every read names both a user and explicit dataset_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.
Reading your own dataset needs no grant. 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.
The two refusals are the default posture. Pointing the same recall at 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.
One "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.
The tenant is built in a forced order, and the first role grant is refused. Each line is one constraint being satisfied: tenant, active tenant, role, new user, tenant membership, role membership, and finally 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.
The grant lands once the dataset lives in the tenant, and 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_COMPLETION recall 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.pdf from the sibling data/ folder next to it
  • Point it at a scratch instance: it opens with prune_data() and prune_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

After the two prunes have emptied the instance, 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

Passing 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

Each 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

The same recall that just worked against 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

The grant reads as who gets it, on what, which permission, and who is authorizing — the last argument is the acting user, and the call is refused unless that user is allowed to share the dataset. One "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

Granting per user does not scale past a handful of colleagues, so 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

Handing the role the original 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

The fix is to put the data where the role can be given access to it: 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

The run is a single pass with no arguments and no interactive steps; What to Expect above walks through its output.

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.