Skip to content

feat(authorization): add a grants-only pre-check for staged writes - #1013

Merged
abnegate merged 1 commit into
mainfrom
feat/authorize-grants
Oct 13, 2026
Merged

abnegate merged 1 commit into
mainfrom
feat/authorize-grants

Conversation

@abnegate

@abnegate abnegate commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

What

Adds Database::authorizeGrants(string $collection, Document $document, ?callable $resolve = null): void (8.1.1, additive).

It runs only the grant part of authorizeCreate() / authorizeUpdate(): a related document given as a document may add permissions only for roles the caller holds. Same preparation and same refusals:

  • related documents are first prepared by the registered Hook\Nested hooks (a hook-assigned $id is judged new);
  • $resolve(collection, id) gives the staged view of a related document, as for authorizeCreate() (empty = staged for deletion = new);
  • an unheld role throws Exception\Authorization (Permissions must be one of: (<roles>)), malformed permissions throw Exception\Structure.

It checks no create or update permission on the collection, the document, or any related collection/document, and writes nothing. It does nothing (and reads nothing) unless setCheckGrants(true), authorization is enabled, and the relationship hook is attached and enabled; while it runs, a missing collection throws Exception\NotFound.

There is no $old parameter: the grant check never reads the stored parent, so the result is the same whether the later write creates or updates. $document can be the full document or the changes.

Why

A caller that stages writes and pre-checks them before commit (a staged single upsert in a transaction) must report a related document granting an unheld role even when the caller also lacks permission on the parent collection. authorizeCreate() / authorizeUpdate() check the parent first, so the grant refusal could not surface at staging. Callers run authorizeGrants() first, then the create/update pre-check.

Tests

tests/unit/Relationships/AuthorizeGrantsTest.php, Memory + SQLite × all four relationship types (56 cases):

  • parent permission missing: authorizeCreate/authorizeUpdate refuse the parent, authorizeGrants reports the grant (the motivating case);
  • parity: new/stored/array/no-permission/dropped/malformed related documents give exactly the refusal of authorizeCreate() and authorizeUpdate();
  • no create/update/read check on parent or related collections and documents, and no check of the top-level document's own permissions;
  • staged view through $resolve (staged permissions count as current, staged deletion is new), parity with authorizeCreate();
  • Hook\Nested: hooks run parent-first at every depth, rewritten IDs judged new and not resolved, grants checked as deep as the write relates, caller's document unchanged; hook-assigned stored ID judged new;
  • no-op while grants are off, inside withCheckGrants(false), under Authorization::skip(), under skipRelationships() (no hook, no resolver, no collection read); Exception\NotFound for a missing collection while on.

Fail-first: without the change all 56 fail (Call to undefined method Database::authorizeGrants()).

Docs: CHANGELOG 8.1.1 (Added), UPGRADE grant-check section.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added authorizeGrants() to check permissions granted by related documents without checking create or update permissions on the target document or collection. Checks run only when grant checking, authorization, and the relationship hook are enabled.
  • Documentation
    • Clarified which operations pass related documents to relationship hooks and which operations check their grants.

A caller that stages a write and pre-checks it before commit, such as a
staged upsert, needs to report a related document granting a role the
caller does not hold even when the caller also lacks permission on the
parent. authorizeCreate() and authorizeUpdate() check the parent first,
so the grant refusal never surfaced at staging.

authorizeGrants() runs only the grant part of those pre-checks: the
Hook\Nested preparation, the staged view through $resolve and the same
Exception\Authorization and Exception\Structure refusals, with no create
or update permission check and no write. The grant check never reads
the stored parent, so it takes no $old and is the same for a create and
an update. While grants are not checked it does nothing and reads
nothing, matching the toggle being off everywhere else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 13, 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: d98a6a5e-108d-4a70-8422-1c73a6300de1
📥 Commits

Reviewing files that changed from the base of the PR and between f1a8753 and 74cf8e1.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • UPGRADE.md
  • src/Database/Database.php
  • src/Database/Hook/Nested.php
  • src/Database/Trait/Documents.php
  • tests/unit/Relationships/AuthorizeGrantsTest.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

Adds Database::authorizeGrants() to check grants on nested related documents. The method uses nested-document preparation and an optional staged resolver. It does not check create or update permissions. Documentation and tests describe and exercise its conditions and behavior.

Changes: Related-document grant authorization

Layer / File(s) Summary
Add and validate related-document grant checks
src/Database/Trait/Documents.php, src/Database/Database.php, src/Database/Hook/Nested.php, UPGRADE.md, tests/unit/Relationships/AuthorizeGrantsTest.php, CHANGELOG.md
Adds authorizeGrants() and documents its relationship-hook preparation, staged resolution, and grant-check behavior. Tests cover adapters, relationship types, permission outcomes, and bypass conditions.

Priority: ⬇️ Low

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Database as Database::authorizeGrants
  participant Collection
  participant Nested as Hook::Nested
  participant Resolver as Optional resolver
  Database->>Database: Check grant, authorization, and hook settings
  Database->>Collection: Load collection silently
  Database->>Nested: Prepare nested document copy
  opt Resolver provided
    Nested->>Resolver: Resolve staged related documents
    Resolver-->>Nested: Return staged view
  end
  Nested->>Nested: Check grants on related documents
Loading

Suggested reviewers: claude

Merge Risk: ⚪ Minimal · up to 74cf8

The grants-only check appears ready to merge after normal checks; no actionable issue was identified.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 4 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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: adding a grants-only authorization pre-check for staged writes.
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 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 4 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 c48811f into main Oct 13, 2026
24 checks passed
@abnegate
abnegate deleted the feat/authorize-grants branch October 13, 2026 12:54
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