Scenarios and buckets belong to users (per-user ownership) #50

Closed
opened 2026-06-22 18:50:06 +02:00 by myrmidex · 0 comments
Owner

Context

Discovered during #32 (allocation preview endpoint) pre-commit review. Currently there is no per-user ownership in the data model: Scenario has no owner relationship, and its #[ApiResource] (and the new projection-preview endpoint) gate only on is_granted('ROLE_USER'). This means any authenticated user can read/preview/mutate any scenario, bucket, or stream by iterating UUIDs — there is no object-level authorization.

This is a systemic, pre-existing gap (not introduced by #32). None of these models should be shared between users.

Entity inventory (complete): the only API-exposed, scenario-scoped resources are Scenario, Bucket, and Stream — all three are in scope. User is the owner itself (not API-exposed). There are no other entities.

Goal

Scenarios, buckets, and streams belong to a single owning user. A user can only see and operate on their own. Cross-user access is indistinguishable from a non-existent resource (404, never 403 — no existence oracle).

Locked decisions

  • Non-owner access → 404 (not 403). Achieved via Doctrine ORM query extensions (WHERE owner = :currentUser) so non-owned rows don't exist for the query → API Platform's provider 404s naturally. NOT voters / security expressions (those return 403 and leak existence). Consistent with the #32 preview-endpoint 404 choice.
  • Owner lives on Scenario only. Bucket and Stream ownership is transitive via scenario.owner — no owner FK on Bucket or Stream (single source of truth, no sync risk).
  • Owner auto-set on create from the authenticated user (persist-processor decorator), and stripped from writable fields so a client-supplied owner is ignored.
  • Migration: empty-table assumption — owner FK added nullable: false directly (tests build their own fixtures per-test; no backfill needed).

Scope / stories

  1. Scenario has an owner, set from session on create — owner FK + migration + persist-processor decorator + owner stripped from writable + persistence/POST tests.
  2. Scenario reads scoped to owner, 404 for non-owners — Doctrine query extension (item + collection).
  3. Buckets scoped transitively via scenario.owner — extension joins bucket→scenario→owner; bucket-create under a foreign scenario IRI → 404.
  4. Preview endpoint enforces ownershipPreviewProcessor injects current user, owner check → NotFoundHttpException (404).
  5. Streams scoped transitively via scenario.owner — same treatment. NOTE: Stream has BOTH a scenario and an optional bucket FK; on create, both the scenario IRI and (if present) the bucket IRI must belong to the caller, else 404 (otherwise a non-owner could attach a stream to another user's bucket).

Open item (resolves during build)

  • Exact status when creating a bucket/stream under a foreign (non-owned) scenario/bucket IRI — the query extension makes it unresolvable, but API Platform may surface 404/400/422. Pin the actual behaviour with a test in Stories 3/5; add a normalizing guard to force 404 if needed.

Out of scope

  • Sharing / collaboration (single-owner only).
  • Roles beyond owner.
## Context Discovered during #32 (allocation preview endpoint) pre-commit review. Currently there is **no per-user ownership** in the data model: `Scenario` has no owner relationship, and its `#[ApiResource]` (and the new projection-preview endpoint) gate only on `is_granted('ROLE_USER')`. This means **any authenticated user can read/preview/mutate any scenario, bucket, or stream** by iterating UUIDs — there is no object-level authorization. This is a systemic, pre-existing gap (not introduced by #32). None of these models should be shared between users. **Entity inventory (complete):** the only API-exposed, scenario-scoped resources are `Scenario`, `Bucket`, and `Stream` — all three are in scope. `User` is the owner itself (not API-exposed). There are no other entities. ## Goal Scenarios, buckets, and streams belong to a single owning user. A user can only see and operate on their own. Cross-user access is indistinguishable from a non-existent resource (**404, never 403** — no existence oracle). ## Locked decisions - **Non-owner access → 404** (not 403). Achieved via **Doctrine ORM query extensions** (`WHERE owner = :currentUser`) so non-owned rows don't exist for the query → API Platform's provider 404s naturally. NOT voters / `security` expressions (those return 403 and leak existence). Consistent with the #32 preview-endpoint 404 choice. - **Owner lives on `Scenario` only.** Bucket and Stream ownership is **transitive** via `scenario.owner` — no owner FK on Bucket or Stream (single source of truth, no sync risk). - **Owner auto-set on create** from the authenticated user (persist-processor decorator), and **stripped from writable fields** so a client-supplied owner is ignored. - **Migration:** empty-table assumption — owner FK added `nullable: false` directly (tests build their own fixtures per-test; no backfill needed). ## Scope / stories 1. **Scenario has an owner, set from session on create** — owner FK + migration + persist-processor decorator + owner stripped from writable + persistence/POST tests. 2. **Scenario reads scoped to owner, 404 for non-owners** — Doctrine query extension (item + collection). 3. **Buckets scoped transitively via `scenario.owner`** — extension joins bucket→scenario→owner; bucket-create under a foreign scenario IRI → 404. 4. **Preview endpoint enforces ownership** — `PreviewProcessor` injects current user, owner check → `NotFoundHttpException` (404). 5. **Streams scoped transitively via `scenario.owner`** — same treatment. NOTE: Stream has BOTH a `scenario` and an optional `bucket` FK; on create, **both** the scenario IRI and (if present) the bucket IRI must belong to the caller, else 404 (otherwise a non-owner could attach a stream to another user's bucket). ## Open item (resolves during build) - Exact status when creating a bucket/stream under a foreign (non-owned) scenario/bucket IRI — the query extension makes it unresolvable, but API Platform may surface 404/400/422. Pin the actual behaviour with a test in Stories 3/5; add a normalizing guard to force 404 if needed. ## Out of scope - Sharing / collaboration (single-owner only). - Roles beyond owner.
myrmidex added the
enhancement
label 2026-06-22 18:50:06 +02:00
myrmidex self-assigned this 2026-06-22 18:50:06 +02:00
myrmidex added this to the v0.3.0 milestone 2026-06-22 21:09:36 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/buckets#50
No description provided.