Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/topoteretes-cognee-permissions/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -17% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -6% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 54% | 0% |
ENABLE_BACKEND_ACCESS_CONTROL decides whether any of this runs:
true (default): multi-tenant mode. Every API call requires auth, everydataset 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, andTenant 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 inpermissions/permission_types.py: read, write, delete, share. share is the meta-permission: it gates granting/revoking access for others.
UserRole and UserTenant linkusers 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.
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).
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.
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:
dataset you cannot read yields [] — deliberate, to avoid leaking which datasets exist. When debugging "search returns nothing", check grants before checking the graph.
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.
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.
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.
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 filtering
cognee/modules/data/methods/(get_authorized_existing_datasets, create_authorized_dataset)
cognee/api/v1/visualize/memory_provenance.pycognee/api/v1/permissions/routers/get_permissions_router.py| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 30,182 | 20,316 | -33% | 1 | 1 | 0% | 4,281 | 5,106 | +19% | 0 | 0 | — |
case-07 | fail→pass | 16,012 | 11,232 | -30% | 1 | 1 | 0% | 1,788 | 3,276 | +83% | 0 | 0 | — |
case-02 | fail→pass | 40,668 | 19,155 | -53% | 1 | 1 | 0% | 5,767 | 4,764 | -17% | 0 | 0 | — |
case-03 | fail→pass | 46,153 | 29,864 | -35% | 1 | 1 | 0% | 7,328 | 6,904 | -6% | 0 | 0 | — |
case-04 | fail→pass | 22,564 | 15,117 | -33% | 1 | 1 | 0% | 2,714 | 4,183 | +54% | 0 | 0 | — |
case-05 | fail→pass | 14,808 | 8,959 | -39% | 1 | 1 | 0% | 1,681 | 2,883 | +72% | 0 | 0 | — |
case-06 | fail→pass | 18,557 | 18,442 | -1% | 1 | 1 | 0% | 2,348 | 4,644 | +98% | 0 | 0 | — |
case-08 | fail→pass | 13,321 | 7,212 | -46% | 1 | 1 | 0% | 1,159 | 2,688 | +132% | 0 | 0 | — |
case-09 | fail→pass | 19,578 | 13,007 | -34% | 1 | 1 | 0% | 2,365 | 3,663 | +55% | 0 | 0 | — |
case-10 | fail→pass | 16,093 | 9,708 | -40% | 1 | 1 | 0% | 1,643 | 2,871 | +75% | 0 | 0 | — |
case-11 | pass→pass | 21,780 | 14,348 | -34% | 1 | 1 | 0% | 2,489 | 3,758 | +51% | 0 | 0 | — |
case-12 | pass→pass | 16,506 | 9,531 | -42% | 1 | 1 | 0% | 1,825 | 2,928 | +60% | 0 | 0 | — |
case-13 | fail→pass | 16,884 | 10,733 | -36% | 1 | 1 | 0% | 2,049 | 3,233 | +58% | 0 | 0 | — |
case-14 | fail→pass | 18,078 | 15,263 | -16% | 1 | 1 | 0% | 2,142 | 3,948 | +84% | 0 | 0 | — |
case-15 | fail→pass | 17,843 | 11,183 | -37% | 1 | 1 | 0% | 1,877 | 3,373 | +80% | 0 | 0 | — |
case-16 | fail→pass | 16,870 | 7,559 | -55% | 1 | 1 | 0% | 2,599 | 2,698 | +4% | 0 | 0 | — |
case-17 | pass→pass | 18,197 | 10,246 | -44% | 1 | 1 | 0% | 1,983 | 3,104 | +57% | 0 | 0 | — |
case-18 | fail→pass | 16,885 | 7,465 | -56% | 1 | 1 | 0% | 3,074 | 2,574 | -16% | 0 | 0 | — |
case-19 | fail→pass | 14,176 | 2,335 | -84% | 1 | 1 | 0% | 1,739 | 2,639 | +52% | 0 | 0 | — |
case-20 | pass→pass | 29,963 | 12,869 | -57% | 1 | 1 | 0% | 2,441 | 3,442 | +41% | 0 | 0 | — |
case-21 | fail→fail | 16,888 | 15,646 | -7% | 1 | 1 | 0% | 1,984 | 3,887 | +96% | 0 | 0 | — |
case-22 | fail→fail | 19,895 | 20,365 | +2% | 1 | 1 | 0% | 2,509 | 4,868 | +94% | 0 | 0 | — |
case-23 | fail→fail | 22,496 | 14,469 | -36% | 1 | 1 | 0% | 1,546 | 3,972 | +157% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +70 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.