Skip to content

Require read permission for existing relationship references - #971

Open
HarshMN2345 wants to merge 2 commits into
mainfrom
codex/fix-relationship-reference-read
Open

HarshMN2345 wants to merge 2 commits into
mainfrom
codex/fix-relationship-reference-read

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Sep 14, 2026

Copy link
Copy Markdown
Member

Creating a document with a relationship ID can persist a reference to an existing document the caller cannot read. The permission-filtered lookup returns an empty document, and the early return leaves the supplied foreign key in the new parent.

Distinguish an unreadable existing target from a missing target and reject the former with an authorization error. Preserve missing references needed when nested writes link back to a parent that has not been inserted yet.

The shared adapter regression covers all four relationship types in one-way and two-way configurations, verifies that rejected writes leave no parent, then checks a readable reference and nested creation. It fails against the current implementation and passes locally with SQLite, shared-table SQLite, and Mirror-backed SQLite (three tests, 120 assertions). Changed-file syntax and Pint checks pass; PHPStan level 7 reports no new diagnostics compared with the unchanged base. GitHub CI passes on the current head: all 16 adapter suites, unit tests, Pint, and CodeQL. The PostgreSQL and Mirror job logs confirm the regression executed.

Addresses appwrite/appwrite#7263.

Appwrite must adopt a released package version to receive this change.

@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 58 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 1ca2cacf-c6f6-4569-8878-bdafd151706d

📥 Commits

Reviewing files that changed from the base of the PR and between 64f5257 and f66f1a6.

📒 Files selected for processing (2)
  • src/Database/Database.php
  • tests/e2e/Adapter/Scopes/RelationshipTests.php

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.

@HarshMN2345
HarshMN2345 marked this pull request as ready for review September 14, 2026 12:33
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

The PR is not safe to merge until the relationship-write existence oracle is removed and the repository's observable-testing requirement is satisfied.

Fix All in Claude CodeFindings

  1. P1 Security Relationship IDs Leak Existence
  2. P2 Test Copies Internal Bypasses
Fix with agent prompt
### Issue 1
src/Database/Database.php:6247-6252
A caller allowed to create or update a parent can supply arbitrary relationship IDs. An existing but unreadable ID now produces a distinct authorization error, while a nonexistent ID follows the early return and lets the write continue. This defeats `getDocument()`'s existing behavior of treating missing and unreadable records alike, allowing callers to enumerate hidden document IDs. The missing-reference exception should be limited to a trusted nested-write state, or both externally supplied cases must remain indistinguishable.

**How this was verified:** Relationship IDs from parent writes reach this lookup, and the permission-skipped result produces an exception only when the unreadable target exists.

### Issue 2
tests/e2e/Adapter/Scopes/RelationshipTests.php:1946-1949
This test reproduces the production implementation's exact `authorization->skip(skipRelationships(getDocument(...)))` helper composition merely to check whether the parent persisted. That violates the repository directive to test observable behavior rather than mirror implementation details. Because the parent already grants `read(Role::any())`, a normal `getDocument($parents, 'reference')` can verify the same rollback behavior without coupling the regression to authorization and relationship-resolution internals. This repository requirement must be satisfied before merging.

```suggestion
            $stored = $database->getDocument($parents, 'reference');
            $this->assertTrue($stored->isEmpty());
```

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • Distinguishes unreadable relationship targets from absent targets during relationship writes.
  • Verifies rejection, rollback, readable linking, and nested document creation.
  • Introduces a document-existence disclosure through externally distinguishable outcomes.
  • Includes an implementation-coupled persistence assertion that should use the readable public document lookup instead.

Reviews (1) · Last reviewed commit: "Assert persisted relationship state afte..."

Comment thread src/Database/Database.php
Comment on lines +6247 to +6252
$exists = $this->authorization->skip(
fn () => $this->skipRelationships(fn () => $this->getDocument($relatedCollection->getId(), $relationId))
);
if (!$exists->isEmpty()) {
throw new AuthorizationException('Missing read permission for the related document.');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Relationship IDs Leak Existence

A caller allowed to create or update a parent can supply arbitrary relationship IDs. An existing but unreadable ID now produces a distinct authorization error, while a nonexistent ID follows the early return and lets the write continue. This defeats getDocument()'s existing behavior of treating missing and unreadable records alike, allowing callers to enumerate hidden document IDs. The missing-reference exception should be limited to a trusted nested-write state, or both externally supplied cases must remain indistinguishable.

How this was verified: Relationship IDs from parent writes reach this lookup, and the permission-skipped result produces an exception only when the unreadable target exists.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Database/Database.php
Line: 6247-6252

Comment:
**Relationship IDs Leak Existence**

A caller allowed to create or update a parent can supply arbitrary relationship IDs. An existing but unreadable ID now produces a distinct authorization error, while a nonexistent ID follows the early return and lets the write continue. This defeats `getDocument()`'s existing behavior of treating missing and unreadable records alike, allowing callers to enumerate hidden document IDs. The missing-reference exception should be limited to a trusted nested-write state, or both externally supplied cases must remain indistinguishable.

**How this was verified:** Relationship IDs from parent writes reach this lookup, and the permission-skipped result produces an exception only when the unreadable target exists.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +1946 to +1949
$stored = $database->getAuthorization()->skip(
fn () => $database->skipRelationships(fn () => $database->getDocument($parents, 'reference'))
);
$this->assertTrue($stored->isEmpty());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test Copies Internal Bypasses

This test reproduces the production implementation's exact authorization->skip(skipRelationships(getDocument(...))) helper composition merely to check whether the parent persisted. That violates the repository directive to test observable behavior rather than mirror implementation details. Because the parent already grants read(Role::any()), a normal getDocument($parents, 'reference') can verify the same rollback behavior without coupling the regression to authorization and relationship-resolution internals. This repository requirement must be satisfied before merging.

Suggested change
$stored = $database->getAuthorization()->skip(
fn () => $database->skipRelationships(fn () => $database->getDocument($parents, 'reference'))
);
$this->assertTrue($stored->isEmpty());
$stored = $database->getDocument($parents, 'reference');
$this->assertTrue($stored->isEmpty());

Context Used: Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/Adapter/Scopes/RelationshipTests.php
Line: 1946-1949

Comment:
**Test Copies Internal Bypasses**

This test reproduces the production implementation's exact `authorization->skip(skipRelationships(getDocument(...)))` helper composition merely to check whether the parent persisted. That violates the repository directive to test observable behavior rather than mirror implementation details. Because the parent already grants `read(Role::any())`, a normal `getDocument($parents, 'reference')` can verify the same rollback behavior without coupling the regression to authorization and relationship-resolution internals. This repository requirement must be satisfied before merging.

```suggestion
            $stored = $database->getDocument($parents, 'reference');
            $this->assertTrue($stored->isEmpty());
```

**Context Used:** Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

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