The cognee permission system
The master switch
ENABLE_BACKEND_ACCESS_CONTROL decides whether any of this runs:
true (default): multi-tenant mode. Every API call requires auth, every
dataset operation is permission-checked, and each user+dataset pair gets
isolated graph/vector/relational databases (tracked in the
DatasetDatabase model, supported backends: Kuzu, LanceDB, SQLite,
Postgres).
false: single-user mode. Permission checks short-circuit to allowed,
there is no per-dataset isolation, and every user's operations resolve
to the same shared databases and datasets. Authentication is a separate
knob: REQUIRE_AUTHENTICATION. Unset, it inherits this switch (so
turning access control off also turns auth off) — but if
REQUIRE_AUTHENTICATION=true is set, endpoints still demand a login;
authenticated users are identified but not isolated, all pointing at
the same data. The reverse misconfiguration
(REQUIRE_AUTHENTICATION=false with access control on) is ignored: auth
is forced on with a warning, because multi-tenant isolation is
meaningless without identity (get_authenticated_user.py).
The core model: principals, permissions, ACL grants
Everything reduces to one relation — a grant: principal × permission
× dataset, stored as one ACL row (modules/users/models/ACL.py).
- Principal (
Principal.py) is polymorphic: User, Role, and
Tenant all inherit from it. Any of the three can hold a grant, which is
how role-wide and tenant-wide access work — one ACL row covers every
member.
- Permission (
Permission.py) is one of exactly four names, defined in
permissions/permission_types.py: read, write, delete, share.
share is the meta-permission: it gates granting/revoking access for
others.
- Membership is separate from grants:
UserRole and UserTenant link
users into roles/tenants. A user's effective access is the union of their
own grants and the grants of every role/tenant they belong to.
How grants come into existence
- Dataset creation (
modules/data/methods/create_authorized_dataset.py):
the creating user is granted all four permissions on the new dataset.
If the user has a parent_user_id (sub-users/agent identities), the
parent is auto-granted all four as well — parents always see their
children's datasets.
- Explicit sharing (
permissions/methods/ authorized_give_permission_on_datasets.py): the caller must hold
share on the target datasets, then any principal (user, role, or
tenant) can be granted any permission. Revocation mirrors this
(authorized_revoke_permission_on_datasets.py).
- Capabilities — tenant-scoped grants of actions, not data (landing
via PR #4302, currently in review): a new
principal_capabilities
table, keyed on (principal, tenant, capability). Where an ACL row
grants access to a dataset, a capability grants an action inside a
tenant — the first one being manage_users. The catalog of capability
names is code (CAPABILITY_TYPES in permission_types.py), not a
database table, "because the code is what gives each name meaning";
only the assignment of a capability to a principal is data. tenant_id
is stored on every row because a user can belong to multiple tenants:
it pins each grant to the user's membership in one specific tenant, so
holding a capability in one tenant never carries over to the same
user's other tenants. Resolution
(get_effective_capabilities(user, tenant)) returns the union of what
the tenant grants all of its members, what the user's roles in that
tenant grant, and what the user was granted personally — there is no
deny in the model, resolution is gated on actual tenant membership, and
the tenant owner short-circuits as holding every capability.
Grant/revoke endpoints ride the permissions router.
Where permissions are enforced
The single chokepoint for dataset resolution is
get_authorized_existing_datasets(datasets, permission, user) — every
entrypoint resolves names/IDs through it with the permission it needs:
| Operation |
Required permission |
Enforcement path |
add / cognify / remember |
write |
dataset resolution before the pipeline runs |
search / recall / visualize |
read |
dataset resolution; retrieval is restricted to documents of readable datasets |
delete / prune of a dataset |
delete |
datasets.py resolves with "delete" |
| grant/revoke for others |
share |
authorized_give/revoke_permission_on_datasets |
Two behaviors worth knowing:
- Denied reads return empty results, not 403. A search against a
dataset you cannot read yields
[] — deliberate, to avoid leaking which
datasets exist. When debugging "search returns nothing", check grants
before checking the graph.
Roles, tenants, and who may manage them
- User management (listing tenant users, assigning/removing roles,
adding/removing users) is allowed for the tenant owner always, and
today for members of roles named in
USER_MANAGEMENT_ALLOWED_ROLE_NAMES
(currently {"admin"}, permissions/permission_types.py). That
name-matching is a known footgun — any customer group that happens to be
called "admin" gets user management — and PR #4302 replaces it: the
check becomes "does the requester hold the manage_users capability in
this tenant" (owner always passes), with the role-name match kept only
as a deprecated fallback so tenants upgrading from the old check don't
lose user management until their admin role is granted the capability.
- Role visibility: members of a role can see the role itself and their
co-members; anyone with user-management permission sees all
(
tenants/methods/get_users_in_role.py). Lookups are tenant-scoped — a
role id from another tenant cannot be used to read that tenant's members.
The grant records in memory provenance (the new grant view)
api/v1/visualize/memory_provenance.py surfaces the ACL grants as
first-class graph data. Each grant becomes an AclGrantRecord:
{"principal_id": ..., "principal_kind": "user" | "role" | "tenant", "permission": ...}
and is rendered into the provenance graph as an edge from the principal
node to the dataset, with the permission mapped to a relation name
(_ACL_EDGE_RELATIONS):
| permission |
provenance edge |
| read |
reads |
| write |
writes |
| delete |
can_delete |
| share |
can_share |
Grants are rendered (never dropped) even when the principal is unknown,
because "an ACL row exists because someone granted it". The view is exposed
through the schema router (get_schema_router.py):
visualize_memory_provenance (HTML) and get_memory_provenance_payload
(JSON) — this is where you see the permission state of a memory rather
than query it.
HTTP API surface (api/v1/permissions/routers/get_permissions_router.py)
| Endpoint |
What it does |
POST /permissions/datasets/{principal_id} |
grant a permission on datasets to a principal (requires share) |
DELETE /permissions/datasets/{principal_id} |
revoke a permission |
POST /permissions/roles · DELETE /permissions/roles/{role_id} |
create/delete a role |
POST/DELETE /permissions/users/{user_id}/roles |
add/remove a user to/from a role |
POST /permissions/users/{user_id}/tenants |
add a user to a tenant |
GET /permissions/tenants/{tenant_id}/roles/{role_id}/users |
members of a role (self-visible to members) |
GET /permissions/tenants/{tenant_id}/roles/users/{user_id} |
a user's roles |
GET /permissions/tenants/{tenant_id}/users |
users in a tenant |
GET /permissions/tenants/me |
the caller's tenants |
Key files map
- Models:
cognee/modules/users/models/ — ACL, Principal, Permission,
Role, Tenant, UserRole, UserTenant, DatasetDatabase (and
PrincipalCapability once #4302 lands)
- Methods:
cognee/modules/users/permissions/methods/ — grant/revoke,
checks, dataset resolution, document filtering
- Enforcement chokepoint:
cognee/modules/data/methods/
(get_authorized_existing_datasets, create_authorized_dataset)
- Grant provenance view:
cognee/api/v1/visualize/memory_provenance.py
- HTTP API:
cognee/api/v1/permissions/routers/get_permissions_router.py
1---2name: cognee-permissions3description: Use when working with cognee's permission system — understanding or changing how users, roles, and tenants get access to datasets, how ACL grants work, where permissions are enforced in add/cognify/search/delete, and how the grant records surface in the memory-provenance view.4---5
6# The cognee permission system
7
8## The master switch
9
10`ENABLE_BACKEND_ACCESS_CONTROL` decides whether any of this runs:
11
12- `true` (default): multi-tenant mode. Every API call requires auth, every
13 dataset operation is permission-checked, and each user+dataset pair gets
14 isolated graph/vector/relational databases (tracked in the
15 `DatasetDatabase` model, supported backends: Kuzu, LanceDB, SQLite,
16 Postgres).
17- `false`: single-user mode. Permission checks short-circuit to allowed,
18 there is no per-dataset isolation, and **every user's operations resolve
19 to the same shared databases and datasets**. Authentication is a separate
20 knob: `REQUIRE_AUTHENTICATION`. Unset, it inherits this switch (so
21 turning access control off also turns auth off) — but if
22 `REQUIRE_AUTHENTICATION=true` is set, endpoints still demand a login;
23 authenticated users are identified but *not isolated*, all pointing at
24 the same data. The reverse misconfiguration
25 (`REQUIRE_AUTHENTICATION=false` with access control on) is ignored: auth
26 is forced on with a warning, because multi-tenant isolation is
27 meaningless without identity (`get_authenticated_user.py`).
28
29## The core model: principals, permissions, ACL grants
30
31Everything reduces to one relation — **a grant**: *principal* × *permission*
32× *dataset*, stored as one `ACL` row (`modules/users/models/ACL.py`).
33
34- **Principal** (`Principal.py`) is polymorphic: `User`, `Role`, and
35 `Tenant` all inherit from it. Any of the three can hold a grant, which is
36 how role-wide and tenant-wide access work — one ACL row covers every
37 member.
38- **Permission** (`Permission.py`) is one of exactly four names, defined in
39 `permissions/permission_types.py`: `read`, `write`, `delete`, `share`.
40 `share` is the meta-permission: it gates granting/revoking access for
41 others.
42- **Membership** is separate from grants: `UserRole` and `UserTenant` link
43 users into roles/tenants. A user's effective access is the union of their
44 own grants and the grants of every role/tenant they belong to.
45
46## How grants come into existence
47
481. **Dataset creation** (`modules/data/methods/create_authorized_dataset.py`):
49 the creating user is granted **all four permissions** on the new dataset.
50 If the user has a `parent_user_id` (sub-users/agent identities), the
51 parent is auto-granted all four as well — parents always see their
52 children's datasets.
532. **Explicit sharing** (`permissions/methods/
54 authorized_give_permission_on_datasets.py`): the caller must hold
55 `share` on the target datasets, then any principal (user, role, or
56 tenant) can be granted any permission. Revocation mirrors this
57 (`authorized_revoke_permission_on_datasets.py`).
583. **Capabilities — tenant-scoped grants of actions, not data** (landing
59 via PR #4302, currently in review): a new `principal_capabilities`
60 table, keyed on `(principal, tenant, capability)`. Where an ACL row
61 grants access to *a dataset*, a capability grants *an action inside a
62 tenant* — the first one being `manage_users`. The catalog of capability
63 names is code (`CAPABILITY_TYPES` in `permission_types.py`), not a
64 database table, "because the code is what gives each name meaning";
65 only the assignment of a capability to a principal is data. `tenant_id`
66 is stored on every row because a user can belong to multiple tenants:
67 it pins each grant to the user's membership in one specific tenant, so
68 holding a capability in one tenant never carries over to the same
69 user's other tenants. Resolution
70 (`get_effective_capabilities(user, tenant)`) returns the union of what
71 the tenant grants all of its members, what the user's roles in that
72 tenant grant, and what the user was granted personally — there is no
73 deny in the model, resolution is gated on actual tenant membership, and
74 the tenant owner short-circuits as holding every capability.
75 Grant/revoke endpoints ride the permissions router.
76
77## Where permissions are enforced
78
79The single chokepoint for dataset resolution is
80`get_authorized_existing_datasets(datasets, permission, user)` — every
81entrypoint resolves names/IDs through it with the permission it needs:
82
83| Operation | Required permission | Enforcement path |
84|---|---|---|
85| `add` / `cognify` / `remember` | `write` | dataset resolution before the pipeline runs |
86| `search` / `recall` / visualize | `read` | dataset resolution; retrieval is restricted to documents of readable datasets |
87| `delete` / prune of a dataset | `delete` | `datasets.py` resolves with `"delete"` |
88| grant/revoke for others | `share` | `authorized_give/revoke_permission_on_datasets` |
89
90Two behaviors worth knowing:
91
92- **Denied reads return empty results, not 403.** A search against a
93 dataset you cannot read yields `[]` — deliberate, to avoid leaking which
94 datasets exist. When debugging "search returns nothing", check grants
95 before checking the graph.
96
97## Roles, tenants, and who may manage them
98
99- **User management** (listing tenant users, assigning/removing roles,
100 adding/removing users) is allowed for the **tenant owner** always, and
101 today for members of roles named in `USER_MANAGEMENT_ALLOWED_ROLE_NAMES`
102 (currently `{"admin"}`, `permissions/permission_types.py`). That
103 name-matching is a known footgun — any customer group that happens to be
104 called "admin" gets user management — and PR #4302 replaces it: the
105 check becomes "does the requester hold the `manage_users` capability in
106 this tenant" (owner always passes), with the role-name match kept only
107 as a deprecated fallback so tenants upgrading from the old check don't
108 lose user management until their `admin` role is granted the capability.
109- **Role visibility**: members of a role can see the role itself and their
110 co-members; anyone with user-management permission sees all
111 (`tenants/methods/get_users_in_role.py`). Lookups are tenant-scoped — a
112 role id from another tenant cannot be used to read that tenant's members.
113
114## The grant records in memory provenance (the new grant view)
115
116`api/v1/visualize/memory_provenance.py` surfaces the ACL grants as
117first-class graph data. Each grant becomes an `AclGrantRecord`:
118
119```python
120{"principal_id": ..., "principal_kind": "user" | "role" | "tenant", "permission": ...}
121```
122
123and is rendered into the provenance graph as an edge from the principal
124node to the dataset, with the permission mapped to a relation name
125(`_ACL_EDGE_RELATIONS`):
126
127| permission | provenance edge |
128|---|---|
129| read | `reads` |
130| write | `writes` |
131| delete | `can_delete` |
132| share | `can_share` |
133
134Grants are rendered (never dropped) even when the principal is unknown,
135because "an ACL row exists because someone granted it". The view is exposed
136through the schema router (`get_schema_router.py`):
137`visualize_memory_provenance` (HTML) and `get_memory_provenance_payload`
138(JSON) — this is where you *see* the permission state of a memory rather
139than query it.
140
141## HTTP API surface (`api/v1/permissions/routers/get_permissions_router.py`)
142
143| Endpoint | What it does |
144|---|---|
145| `POST /permissions/datasets/{principal_id}` | grant a permission on datasets to a principal (requires `share`) |
146| `DELETE /permissions/datasets/{principal_id}` | revoke a permission |
147| `POST /permissions/roles` · `DELETE /permissions/roles/{role_id}` | create/delete a role |
148| `POST/DELETE /permissions/users/{user_id}/roles` | add/remove a user to/from a role |
149| `POST /permissions/users/{user_id}/tenants` | add a user to a tenant |
150| `GET /permissions/tenants/{tenant_id}/roles/{role_id}/users` | members of a role (self-visible to members) |
151| `GET /permissions/tenants/{tenant_id}/roles/users/{user_id}` | a user's roles |
152| `GET /permissions/tenants/{tenant_id}/users` | users in a tenant |
153| `GET /permissions/tenants/me` | the caller's tenants |
154
155## Key files map
156
157- Models: `cognee/modules/users/models/` — `ACL`, `Principal`, `Permission`,
158 `Role`, `Tenant`, `UserRole`, `UserTenant`, `DatasetDatabase` (and
159 `PrincipalCapability` once #4302 lands)
160- Methods: `cognee/modules/users/permissions/methods/` — grant/revoke,
161 checks, dataset resolution, document filtering
162- Enforcement chokepoint: `cognee/modules/data/methods/`
163 (`get_authorized_existing_datasets`, `create_authorized_dataset`)
164- Grant provenance view: `cognee/api/v1/visualize/memory_provenance.py`
165- HTTP API: `cognee/api/v1/permissions/routers/get_permissions_router.py`