# V3 Storage Sharing v1

## Decision

Storage sharing is a first-class product model. A bucket lives in *one*
owner project; other projects gain access through GPUaaS-owned grants that
the storage service compiles into provider policy (WEKA IAM, S3 bucket
policy, etc.). The UI surfaces "shared with this project" as an explicit
posture, not a tooltip on otherwise-identical rows.

This doc describes the canonical product UI affordances and the read-model contract
that backs them.

## Read-model contract (web-side summary)

The browser learns sharing posture from these shapes:

| Field | Source | Meaning |
|---|---|---|
| `V3StorageBucketSummary.project_id` | bucket list / detail | Owner project. The bucket "lives" here. |
| `V3StorageBucketSummary.project_name` | bucket list / detail | Display name of the owner project. |
| `V3StorageGrant` | `GET /api/v1/v3/storage/{id}/grants` | GPUaaS-owned access record (subject + scope + permissions + TTL). |
| `V3StorageAccessAudience` | bucket detail `tabs.access.audiences` | Aggregated audience summary (project_members / service_accounts / workload_bound). |
| `V3StorageCredentialIssueResponse` | `POST /api/v1/v3/storage/{id}/credentials` | One-shot direct S3 credentials with TTL. |

**Owned vs shared**: a bucket is "shared with this project" when its
`project_id` differs from the active session project. The UI computes this
locally — there is intentionally no `bucket.shared` flag. Owner project is
the source of truth; everything else is a grant.

**Provider neutrality**: read models never expose provider raw policy JSON,
provider admin identifiers, or vendor-specific endpoints. The credential
issuance response is the only place provider-direct material is returned,
and only once per call.

## UI affordances

### Storage workbench (`/storage`)

- KPI strip shows owned vs shared count alongside total.
- Two sectioned tables: "Owned by this project" and "Shared with this project".
- Shared rows carry a `shared with` badge and an "Owner project" column.
- Action band routes to the workbench ActionBand entries unchanged.

### Bucket detail (`/storage/{id}`)

- Context strip leads with **Owner project**; when shared, the strip carries
  a `shared with you` badge so the operator never confuses a shared bucket
  for one their project owns.
- New **Grants** tab lists `V3StorageGrant` records: subject kind + id,
  permissions, prefixes (or `(whole bucket)`), expiry, revoked/active.
- New **Issue S3 credentials** action in the resource header opens a modal:
  - Inputs: TTL minutes, client kind (`s3_cli` / `sdk` / `workload_mount`), reason.
  - Result panel renders endpoint, access key id, secret access key,
    optional session token, scope summary, and `credential TTL` countdown.
  - Material is shown once and never persisted (no localStorage, no
    react-query cache, no logging). The dialog clears its issued state on
    close.

### Launch wizards (compute + apps)

- Both wizards use a shared `BucketPicker` component that classifies
  buckets into "Owned by this project" vs "Shared with this project".
- Shared buckets default to **read-only** mount mode. Operators with
  admin/write grant context can flip per bucket if the wizard caller passes
  `allowWritableForShared`.
- The component sets `storage_bucket_ids` (the submit shape) and tracks
  `storage_selections` for per-bucket mount-mode UI state. The backend is
  the source of truth for whether a bucket is mountable writable; the UI
  hint is conservative on purpose.

## Security and audit notes

- The grants list endpoint never returns provider raw policy.
- Credential issuance is audited as `storage.credential.issue` with
  subject + permissions + TTL + reason.
- Grant create / revoke are audited as `storage.grant.create` and
  `storage.grant.revoke`. The UI never bypasses these audit events by
  poking provider APIs directly.
- The dialog input "reason" is mandatory in spirit (defaulted to a
  user-initiated string) and stored on the audit log; operators can pivot
  on it during incident review.

## Test surfaces

- `packages/web/src/components/v3/bucket-picker.tsx` — render is purely
  derived from active project + bucket list; behaviour is testable by
  mocking the active project session and the bucket list query.
- `packages/web/src/components/v3/v3-storage-page.tsx` — `data-variant`
  attribute on each row marks `owned` vs `shared` for E2E assertions.
- `packages/web/src/components/v3/v3-storage-detail-page.tsx` — `Grants`
  tab and the `Issue S3 credentials` button are stable selectors.
- The fail-closed E2E mock harness needs entries for
  `GET /backend/api/v1/v3/storage/{id}/grants` and
  `POST /backend/api/v1/v3/storage/{id}/credentials` once specs assert on
  these surfaces.

## Out of scope

- Cross-tenant sharing UX (federation between tenants is a separate task).
- Granular grant editor (current UI lists existing grants; the create flow
  is intentionally deferred to a follow-up that ships a structured editor).
- Real WEKA / VAST / DDN provider routing — the UI never names these
  backends in user copy. Provider hints come from
  `V3StorageProviderCapability.display_name` only.
