Repository navigation
feat: judge authorizeCreate/authorizeUpdate against a staged view (8.0.3) - #1005
Conversation
A transaction stages its operations in separate requests and commits them later, so the write a pre-check judges runs after the operations staged before it. authorizeCreate() and authorizeUpdate() read related documents from storage, so they judged a document the transaction already deletes as a stored link needing update, and a document it already creates, changes or relinks by its stored permissions and links. The pre-check then refused writes the commit allows and allowed writes the commit refuses. Both now take an optional resolver that gives a related document as the staged operations leave it: an empty document when they delete it (so the nested document is new and needs create on its collection), the staged document when they create or change it (judged by its permissions, readability and links), or null to read the stored one. Every related-document lookup of the pre-check goes through it. Without a resolver the checks are unchanged. The tests stage deletes, updates and creates for every relationship type and assert the pre-check decides what the write decides after the staged writes have run (263 of them fail without the resolver). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applications staging writes need to know what the resolver returns for deleted, changed and untouched documents and that $old follows the same staged view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to This adds an optional staged-view resolver to authorization checks. Behavior is unchanged when the resolver is omitted, and no concrete merge-blocking risk was found. Pre-merge checks |
|
Why
Appwrite stages transaction operations in separate requests and commits them later, and calls
Database::authorizeCreate()/authorizeUpdate()at staging so a refused write is refused when it is staged. Those checks read related documents from storage, but the staged write runs after the operations staged before it. So the pre-check judged a related document the transaction already deletes as a stored link needing update, and a document it already creates, changes or relinks by its stored permissions and links. Example (appwritetestCannotRecreateRelatedWithUnauthorizedPermissions): a transaction stages a delete of child C (one-way one-to-one; child collection grants read/create/delete, not update; C grants update only toteam:admin), then stages the create of a parent nesting{ $id: C, title, $permissions }. The commit creates C anew (create permission), but the pre-check saw stored C and refused with C's update reason.What
User-approved API addition for 8.0.3:
$resolvereturns a related document as the earlier staged writes leave it:Document: they delete it. The nested document is new to the write, so it needs create on its collection (and its nested documents are checked as new). A link by ID to it is judged like a link to a missing document.Document: the staged version, with its$permissionsand not filtered by the caller's read. The caller's read, the update needed to link or change it, and its links (for already-linked detection one level down) are judged from it.null: they leave it alone. The stored document is read as before.Every related-document lookup of the pre-check goes through it: stored-link checks, unreadable-document checks and linked-anew/changed detection.
$oldforauthorizeUpdate()is passed by the caller, so it should follow the same staged view. Without$resolvebehaviour is unchanged, and the checks still never write. The resolver is wrapped in a closure typed?Document, so any other return value is aTypeError.Internals:
Hook\Relationships::authorize()threads the resolver through the recursion (as a parameter, not hook state, so concurrent coroutines don't share it).viewRelated()replaces the directgetDocument()andrefuseUnreadable()calls of the pre-check.refuseHidden()andpermissionInput()are extracted and shared with the write path'srefuseUnreadable()/authorizeLink(), with the same checks and messages.Tests
tests/unit/Relationships/StagedViewAuthorizationTest.php(804 cases, Memory + SQLite):authorizeCreate/authorizeUpdatewith the resolver must return the same refusal (or none) ascreateDocument/updateDocumentrun in a transaction after the staged delete/update/create is applied.nulljudges the stored document, an API key passes whatever the staged view, and the checks write nothing.Fail-first: with
src/atmain, 263 of the 804 cases fail, every staged-view pre-check case (deleted/updated/created). The parity cases pass on both, since they exercise the real write. The existingAuthorizeWriteTest(10) andNestedLinkPermissionsTest(960) still pass. Targeted PHPStan and Pint on the changed files are clean.CHANGELOG 8.0.3 and UPGRADE entries are included.
🤖 Generated with Claude Code
Summary by CodeRabbit