Skip to content

feat: judge authorizeCreate/authorizeUpdate against a staged view (8.0.3) - #1005

Merged
abnegate merged 2 commits into
mainfrom
feat/authorize-staged-view
Oct 12, 2026
Merged

abnegate merged 2 commits into
mainfrom
feat/authorize-staged-view

Conversation

@abnegate

@abnegate abnegate commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

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 (appwrite testCannotRecreateRelatedWithUnauthorizedPermissions): 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 to team: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:

Database::authorizeCreate(string $collection, Document $document, ?callable $resolve = null): void
Database::authorizeUpdate(string $collection, Document $old, Document $document, ?callable $resolve = null): void
// $resolve(string $collection, string $id): ?Document

$resolve returns a related document as the earlier staged writes leave it:

  • an empty 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.
  • a non-empty Document: the staged version, with its $permissions and 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. $old for authorizeUpdate() is passed by the caller, so it should follow the same staged view. Without $resolve behaviour is unchanged, and the checks still never write. The resolver is wrapped in a closure typed ?Document, so any other return value is a TypeError.

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 direct getDocument() and refuseUnreadable() calls of the pre-check. refuseHidden() and permissionInput() are extracted and shared with the write path's refuseUnreadable() / authorizeLink(), with the same checks and messages.

Tests

tests/unit/Relationships/StagedViewAuthorizationTest.php (804 cases, Memory + SQLite):

  • Every relationship type (one-to-one two-way and one-way, one-to-many, many-to-one, many-to-many), by ID and as a document, × 10 staged scenarios: unstaged (collection update / read only), staged delete (create allowed, as in the appwrite case / create missing), staged update revoking, granting, hiding and revealing, and staged create with and without update.
  • Parity: for each case, authorizeCreate/authorizeUpdate with the resolver must return the same refusal (or none) as createDocument/updateDocument run in a transaction after the staged delete/update/create is applied.
  • The appwrite scenario end to end: refused without the resolver, allowed with it, and the commit recreates C with the given permissions.
  • Nested linked detection (a staged update already links a grandchild, so it isn't linked again), a resolver returning null judges the stored document, an API key passes whatever the staged view, and the checks write nothing.

Fail-first: with src/ at main, 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 existing AuthorizeWriteTest (10) and NestedLinkPermissionsTest (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

  • New Features
    • Create and update authorization checks can now evaluate related documents using their staged state, including pending changes and deletions. When no staged-state resolver is supplied, checks continue to use stored documents.
    • Authorization checks remain read-only and do not apply writes.

abnegate and others added 2 commits October 12, 2026 18:51
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>
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: utopia-php/database/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 91d56031-71f4-45e0-9c97-a82c009d704f

📥 Commits

Reviewing files that changed from the base of the PR and between 54dfa6f and a611413.


📒 Files selected for processing (5)
  • CHANGELOG.md
  • UPGRADE.md
  • src/Database/Hook/Relationships.php
  • src/Database/Trait/Documents.php
  • tests/unit/Relationships/StagedViewAuthorizationTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.



📝 Walkthrough

Walkthrough

authorizeCreate() and authorizeUpdate() now accept an optional resolver for related documents. Relationship authorization uses the staged document returned by the resolver, treats an empty result as absent, and falls back to storage when the resolver returns null.

Changes

Staged-view authorization

Layer / File(s) Summary
Resolver API and forwarding
src/Database/Trait/Documents.php, UPGRADE.md, CHANGELOG.md
Both authorization methods accept and forward an optional resolver. The documentation describes resolver results, staged deletions, and passing the staged $old document.
Staged related-document checks
src/Database/Hook/Relationships.php, tests/unit/Relationships/StagedViewAuthorizationTest.php
Relationship checks use resolved or stored documents for read and update permission checks, and propagate the resolver through recursive authorization. Tests cover adapters, relationship types, staged states, permissions, and checks that do not write data.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Documents
  participant Relationships
  participant Resolver
  participant Storage
  Caller->>Documents: Call authorization with resolver
  Documents->>Relationships: Forward resolver
  Relationships->>Resolver: Resolve related document
  alt Resolver returns a document
    Resolver-->>Relationships: Return staged document
  else Resolver returns null
    Relationships->>Storage: Read related document
    Storage-->>Relationships: Return stored document
  end
  Relationships->>Relationships: Check permissions
Loading

Suggested reviewers: claude


Merge Risk: ⚪ Minimal · up to a6114

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 | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 3 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: authorization now evaluates create and update operations against a staged view.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Docstring Coverage

Explanation

Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 3 files. (2 skipped: 2 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@abnegate
abnegate merged commit 8de265b into main Oct 12, 2026
24 checks passed
@abnegate
abnegate deleted the feat/authorize-staged-view branch October 12, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant