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.
690c0ecENABLE_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).Everything reduces to one relation — a grant: principal × permission
× dataset, stored as one ACL row (modules/users/models/ACL.py).
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.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.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.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.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).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.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:
[] — deliberate, to avoid leaking which
datasets exist. When debugging "search returns nothing", check grants
before checking the graph.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.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.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.
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 |
cognee/modules/users/models/ — ACL, Principal, Permission,
Role, Tenant, UserRole, UserTenant, DatasetDatabase (and
PrincipalCapability once #4302 lands)cognee/modules/users/permissions/methods/ — grant/revoke,
checks, dataset resolution, document filteringcognee/modules/data/methods/
(get_authorized_existing_datasets, create_authorized_dataset)cognee/api/v1/visualize/memory_provenance.pycognee/api/v1/permissions/routers/get_permissions_router.pyCopy a source-pinned command for your client. You run it yourself.
Destination: .claude/skills/cognee-permissions · pinned to the source commit
# Run from your project root
git clone https://github.com/topoteretes/cognee.git .skillboard-tmp
git -C .skillboard-tmp checkout 690c0ec023719a2a277dc893cdecfec1ca8012cc
mkdir -p ".claude/skills"
cp -r ".skillboard-tmp/.claude/skills/cognee-permissions" ".claude/skills/"
rm -rf .skillboard-tmpReview the source before running. This copies files into your project; it is not a one-click install and does not verify runtime safety.
sudo apt update && sudo apt install -y gitnpm install -g @anthropic-ai/claude-code# Run from your project root
git clone https://github.com/topoteretes/cognee.git .skillboard-tmp
git -C .skillboard-tmp checkout 690c0ec023719a2a277dc893cdecfec1ca8012cc
mkdir -p ".claude/skills"
cp -r ".skillboard-tmp/.claude/skills/cognee-permissions" ".claude/skills/"
rm -rf .skillboard-tmpDestination: .claude/skills/cognee-permissions
Scanner static-checks@0.1.0 · commit 690c0ec02371. Static checks cannot prove runtime safety – review the source and the exact diff before installing. How checks work.
No static rules matched. This is not a safety guarantee.