fix: Resolve dataset storage under its owner - #4850
Draft
dexters1 wants to merge 1 commit into
Draft
Conversation
dexters1
force-pushed
the
fix/COG-issue-4829-dataset-database-owner-filter
branch
from
September 1, 2026 11:36
4d74249 to
758f713
Compare
Description: The dataset_database existence check filtered by owner_id even though dataset_id is the table's sole primary key, so an ACL grantee calling forget() got a false negative and the fall-through INSERT crashed on the primary key. The deeper cause: the database context trusted the caller-supplied user_id to locate storage (registry row, database paths, data root), so any call site passing a non-owner redirected all of it. Derive the owner from the dataset itself on context entry — a caller identity can no longer redirect storage, and a nonexistent dataset now fails fast instead of provisioning phantom registry rows. Look the registry row up by dataset_id alone, matching the schema. user_id is now optional and consulted only by the new opt-in permission gate: pass permission_type to have the caller's grant verified on entry; when omitted, authorization stays at the API layer as today. Covered by a grantee forget scenario in the delete permission e2e test and new context-manager unit tests. Fixes #4829 Claude-Session: https://claude.ai/code/session_01QHTS3zLjcskWm1qXDPT1Lx
dexters1
force-pushed
the
fix/COG-issue-4829-dataset-database-owner-filter
branch
from
September 1, 2026 11:47
758f713 to
b0cddec
Compare
Comment on lines
219
to
+227
|
|
||
| await dataset_queue().ensure_slot(dataset) | ||
|
|
||
| user = await get_user(user_id) | ||
| # Optional permission gate: checked only when the caller asked for it | ||
| # by passing a permission_type — callers that already authorized at the | ||
| # API layer pass nothing and no check is performed here. | ||
| if permission_type is not None: | ||
| if user_id is None: | ||
| raise CogneeValidationError( |
Contributor
There was a problem hiding this comment.
minor (correctness verified) — The permission check looks correct: it gates entry before storage is touched (line 91), and the owner resolution on line 227 ensures storage always derives from the dataset's owner, not the caller. Good fix for #4829.
One note: _get_dataset_owner_id has no authorization (intentionally, per its docstring), so any caller can learn who owns any dataset. This is probably acceptable since the permission check happens just above, but worth noting for the security model.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #4829.
An ACL grantee calling
forget()on a shared dataset crashed withUNIQUE constraint failed: dataset_database.dataset_id. Two defects, both fixed inside the dataset-resolution mechanism (single-file core change, no call-site churn):_existing_dataset_databasenow looks up bydataset_idalone.dataset_idis the table's sole primary key — one row per dataset by schema design since Dataset permissions #869 — so theowner_idfilter could only ever turn a hit into a false negative, sending the function into a doomed duplicate INSERT (after already running the graph/vector handlers'create_datasetprovisioning for the wrong user).apply_database_context_variablesnow derives the dataset's owner itself instead of trusting the caller-supplieduser_idfor storage. The registry row, physical database paths (databases/<owner>/…), and data root always belonged to the owner; previously they were built from whatever id the call site passed, so one wrong call site (forget passed the caller) redirected all of them — the crash above, and on thememory_onlypaths silently-wrong embedded-DB targets. Now no caller identity can redirect storage.Deriving the owner reads the dataset row, so entering the context for a nonexistent dataset now fails fast with
DatasetNotFoundError— previously it silently provisioned phantom registry rows on SQLite (FK pragma not enabled on that path) or crashed on FK after handler side effects on Postgres.With storage no longer depending on it,
user_idis now optional — it is consulted only by the new opt-in permission gate: passpermission_type="read"/"write"/"delete"/"share"toset_database_global_context_variablesto have the caller's grant verified on entry (apermission_typewithout auser_idis refused). When permission_type is omitted (all current call sites), no permission check is performed here — authorization stays at the API layer exactly as today.Verification
forget(data_id, dataset_id)):IntegrityErrorbefore →{"status": "success"}after, data record removed, single registry row still owned by A, no stray directories for B.cognee/tests/test_delete_permission.py(e2e) extended: granteeforget()end-to-end, plus a direct assertion thatget_or_create_dataset_databasereturns the owner's row to a non-owner.test_context_global_variables.py: storage resolves under the owner for a non-owner caller (no permission check withoutpermission_type), and a denied caller withpermission_typeset is rejected before any storage side effect.tydiagnostics unchanged except one more instance of the pre-existing SQLAlchemyColumntyping noise.Notes
forget.pyis untouched relative todev— the mechanism-level fix makes every call site correct, including the two that passed the caller (forget,cognee_network_visualization).datasetstable now raisesDatasetNotFoundErrorinstead of silently mis-provisioning. An orphaneddataset_databaserow (possible on SQLite where the FK pragma is off) will surface loudly at migration-runner context entry rather than being processed against a zombie dataset.DCO Affirmation
I affirm that all code in every commit of this pull request conforms to the terms of the Topoteretes Developer Certificate of Origin.
https://claude.ai/code/session_01QHTS3zLjcskWm1qXDPT1Lx