P04 View privacy preflight¶
Status: preflight only at integration SHA 7060a83720a339118715ab4e25ca6e805f8d610b. P04 implementation remains gated on the P02 schema and request-context commit. P04 consumes P01/P02 RequestAuthority; it must not add fields or reconstruct authority from claims, profile roles, route IDs, Dashboard headers, or saved preferences.
Fixed authorization shape¶
- Human and delegated View operations require
AccountFeatureId.Viewunder the common evaluator, an enabled account/grant and exact active profile; delegated clients also retain the Application/consent intersection. Service Applications require the live available View service permission and its resource rules, without inventing an account feature grant. Ordinary Applications usingview.personal.*require a validated delegated human binding. - An effective human administrator may select one explicit existing profile's Personal Space. A service administrator Application is the accepted exception: it may read one explicit target profile without a fabricated human/default-profile binding, only while the relevant registered View read service is available, and the selection is audited.
PermissionRegistry.RequiresUserContextforview.personal.read/originals must not erase this exception or weaken ordinary-Application checks. The target profile stays inResourceAuthorizationContext, notRequestAuthority. - Mine always means the active profile's one Personal Space. Shared is the server-owned Shared library, never a union. Ordinary scope options are Mine and Shared; only effective admins/admin Applications receive individual Profile options.
- An explicit unauthorized
scope=profile&scopeProfileId=...is not-found-equivalent. Fallback is allowed only when resolving a stale saved preference, soViewEndpoints.GetScopeAsyncmust preserve whether input was explicit or persisted. - A Gallery grant authorizes that Gallery and its proven manual/smart members and derivatives only, with View still enabled. It never authorizes the owner's root, folders, siblings, or arbitrary assets. Revocation must affect the next asset/original/thumbnail request.
Current call sites and required cutover¶
| Surface | Current path | P04 change |
|---|---|---|
| Scope/preferences/timeline | ViewEndpoints /scopes, /preferences, /assets; ViewScopeResolver; ViewQueryOrchestrator; ViewAssetQueryService -> LocalAssetRepository.QueryTimeline |
Replace ViewRequestProfile(ProfileId, Role) with the P02 request authority accessor; gate View plus view.personal.read or view.shared.read before forming ViewAssetQueryPlan. Add exact admin Profile resolution. Keep physical library IDs backend-only and in cache/query keys. |
| Counts/folders | ViewFolderService.ConfiguredSourcesAsync, CountItems, QueryPaths, QueryItemIds, pins and breadcrumb construction |
Resolve the exact authorized library/source before every count or path query. Pins remain viewer-local. Timeline-policy writes require owner or effective human admin and the filesystem mutation gate; admin read selection alone grants no mutation. Replace the synthetic single Shared source with persisted Shared sources. |
| People/Places | ViewDiscoveryService -> ViewDiscoveryRepository.QueryPeople/QueryPlaces |
Keep authorization before SQL and preserve library/Shared predicates inside the aggregation, so counts, cursors, representative IDs, capability flags, and search cannot include another scope. |
| Asset/thumbnail/original | ViewEndpoints /items/{id}, /content, /thumbnail; ViewResourceAuthorizationService; ViewResourcePersistenceService; LocalAssetRepository.ResolveContent |
Apply View plus resource permission (view.originals.read for content; appropriate personal/shared and Gallery checks for metadata/thumbnail) before lookup. Resolve a verified local_file_sources row belonging to the authorized library/source; global local_files.content_hash dedup must never select a private path sharing the same file ID. Return the same 404 shape for missing and denied. |
| Galleries | ViewEndpoints.MapGalleries; ViewGalleryRepository including IsItemSharedWithProfileAsync |
Gate list/create/read/write with View and view.galleries.read/write. Preserve repository ownership checks and manual/smart membership proof. Validate every added/cover item against the Gallery's exact Personal Space. Share-target discovery remains policy-gated and contains no media facts. |
| Upload/personal mutation | /uploads, flag/lifecycle helpers, ViewLibraryService.UploadAsync |
Upload has no caller-supplied destination today; retain that property and bind it to the active human profile with View + view.upload. Do not let an admin Profile read selection redirect upload, flags, archive, trash, or restore into another profile. |
| Contributions/transfers | contribution endpoints; ViewSharedContributionService; ViewSharedTransferService |
Replace profile-policy-only decisions: submit/cancel remain exact contributor operations; review/decision/retry/direct-add require effective human admin plus the independent policy. Recheck contributor ownership and View at acceptance. Retain preview revision, idempotency, verified-copy-before-publish, and cleanup-pending recovery. |
| Admin sources | /view/admin/profiles/{profileId}/sources and /reconcile |
Replace .RequireAdmin() role checks with effective admin-surface authority. Profile routes bind exactly to the target. Add /view/admin/shared/sources CRUD/reconcile using the stable Shared library and the same path validation/indexing model; no user-facing duplicate library. |
| Collection expansion | CollectionPersonalMediaEndpoints; CollectionPersonalMediaService; CollectionViewSourceRepository.GetAuthorizedProjectionAsync |
Remove ProfileRole.Administrator and profile-only collection decisions. Authorize the collection, View, selected exact profile, and each Gallery/rule source before projection/expansion. A shared Gallery reference exposes only its members; a private smart rule remains owner/exact-admin scoped. Keep the schema ban on individual local-asset references. |
| Dashboard proxy handoff (P05-owned) | ViewMediaProxyEndpoint, ViewMediaGrantService, ViewMediaEngineClient, ViewProfileAssertionHandler |
Treat the opaque grant's profile/library fields as selectors only. Forward the P02 interactive authority snapshot and make the Engine reauthorize the exact asset on every GET/HEAD/range request; no seed Owner or transport-only fallback. |
P02 DDL required before dispatch¶
The current view_sources and local_items require a Personal Space/profile, while view_shared_assets merely marks the same profile-owned item. That cannot represent server-owned Shared folders and makes Shared depend on private identity. P02 should rebuild the pre-beta schema as follows:
CREATE TABLE view_shared_library (
singleton_key INTEGER PRIMARY KEY CHECK (singleton_key = 1),
library_id BLOB NOT NULL UNIQUE,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
-- Rebuild view_sources: add scope_kind CHECK IN ('personal','shared'),
-- nullable personal_space_id, and explicit library_id.
-- Rebuild local_items: add scope_kind; personal_space_id and owner_profile_id nullable.
-- CHECK personal => both personal IDs present; shared => both are NULL.
-- Extend view_shared_assets with origin_item_id BLOB NULL REFERENCES local_items(id) ON DELETE SET NULL;
Use BEFORE INSERT/UPDATE triggers (or composite foreign keys with equivalent strength) on view_sources and local_items: personal rows' (personal_space_id, library_id) must match view_personal_spaces, and personal items' owner_profile_id must also match that space's owner; shared rows' library_id must match singleton view_shared_library. Add partial unique indexes for personal (personal_space_id, source_key) and Shared (library_id, source_key) because NULL does not protect Shared uniqueness. Bootstrap one stable Shared library ID only after checking it does not collide with a Personal Space or other trusted server library identity, and reject later cross-scope collisions; never infer Shared from NULL and never create a synthetic profile.
P04 can own a new IViewSharedLibraryRepository (GetAsync, GetSourcesAsync, UpsertSourceAsync, DeleteSourceAsync) and its Storage implementation, plus explicit scope fields in IViewAssetQueryBackend plans and IViewResourceStore descriptors. P02 owns the DDL/bootstrap. Transfer acceptance should create a distinct Shared-scoped local_items row after all destination files verify, reuse global hash-deduplicated local_files, attach only Shared-library/source local_file_sources, and retain view_shared_assets.original_profile_id plus origin_item_id provenance. Linked personal items remain; managed personal cleanup starts only after Shared publication verifies.
Tests to reuse and gaps¶
Reuse ViewScopeResolverTests, ViewResourceAuthorizationTests, ViewQueryOrchestratorTests, ViewDiscoveryServiceTests, ViewGalleryShareTargetTests, ViewSharedTransferServiceTests, Storage View persistence/discovery/smart-Gallery tests, and CollectionViewSourceRepositoryTests. Reverse the current tests that expect an explicit unauthorized Profile request to fall back.
Add one matrix across scope, counts, folders, People/Places, gallery/list/member, item/thumbnail/original, collection source, contribution ID, and admin source IDs: A cannot distinguish B from missing. Add exact B-selected admin isolation from A/C; same admin account on a non-admin-enabled grant; owner with account View denied; ordinary versus admin Application behavior; Shared read from account View; Gallery member-only access and immediate revoke; spoofed upload/profile/source rejection; verified Shared file-source selection under global hash dedup; multiple Shared sources with no profile owner; and clean read/profile registration producing no directories. Keep all existing transfer safety cases for pending/declined, linked copy, managed move, collision, changed source, and cleanup retry.
Plain English: P04 will make every View list, count, file, Gallery, upload, contribution, and collection reference use one trusted access decision. Private profiles stay separate, while Shared becomes a real server-owned library with multiple explicit sources and preserved transfer provenance.