cognee-permissions · git:20260811.02e4288 · 2026-08-11 · sha256 a3be7a043a83be6d
cognee-permissions git:20260811.02e4288A
Immutable. This exact content is served forever at /api/v1/blob/a3be7a043a83be6d.
---
name: cognee-permissions
description: 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.
---
# 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
1. **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.
2. **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`).
3. **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`:
```python
{"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`