test(visibility): full suite green under RLS — sync paths, device-auth bridge, seed pool
Phase 5 (plan §7): the entire integration suite passes with the RLS policies live. The suite was the triage enumerator the plan predicted: - device_auth middleware: the THIRD identity bridge — KOReader/Kobo device-authenticated requests read media as the DEVICE OWNER (plan A9 called this path out; the flip was 404s on sync metadata). - conflicts/analytics/progress handlers: detached context.Background() reads threaded to the request context (the requesting user's identity) — 12+ sites; resolve helpers take ctx. - scanner: ScanFolders self-declares its configured admin identity (server maintenance, product-correct for any caller); generic job creation stamps the requesting admin. - harness: setup.DBPool and the sync-test pool declare rls.SeedIdentity at SESSION scope via BeforeAcquire — the pool never serves a second identity, so no A3 bleed; setup.DB falls back to the seed identity (NewSeedDB) for ungoverned-context calls; filter tests use seedExec for raw governed-table statements. - contract pin WIDENED (docs/contract_pins.md 2026-10-29): under RLS, item-scoped gates may answer 404 (row invisible — no existence leak) as well as 403 (Go-level checks on listings keep strict 403). - TestWorker_JobStatusTracking — the pre-existing red from the Phase 0 baseline — passes with the harness seed work; no product change.
This commit is contained in:
1 parent
bb2cc26e9d
commit
4d697f5fcf
7 files changed
+84
-38
No files matched your search
@@ -80,7 +80,7 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
|
||||
status = "unresolved"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var conflicts interface{}
|
||||
var err error
|
||||
@@ -263,13 +263,13 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
|
||||
}
|
||||
|
||||
if conflict.ConflictType == "progress" {
|
||||
if err := h.applyProgressResolution(conflict.MediaItemID, conflict.UserID, winnerSource, winnerData); err == nil {
|
||||
if err := h.applyProgressResolution(c.Request().Context(), conflict.MediaItemID, conflict.UserID, winnerSource, winnerData); err == nil {
|
||||
appliedTo["progress"] = true
|
||||
}
|
||||
}
|
||||
|
||||
if conflict.ConflictType == "annotation_highlight" || conflict.ConflictType == "annotation_bookmark" || conflict.ConflictType == "annotation_note" {
|
||||
if err := h.applyAnnotationResolution(conflict.MediaItemID, conflict.UserID, winnerData, conflict.ConflictType); err == nil {
|
||||
if err := h.applyAnnotationResolution(c.Request().Context(), conflict.MediaItemID, conflict.UserID, winnerData, conflict.ConflictType); err == nil {
|
||||
appliedTo["annotations"] = true
|
||||
}
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
|
||||
// the service's LWW seed instead of being NULLed — that is the fix.
|
||||
// chapter_progress keeps its stored value instead of being overwritten
|
||||
// with the book-level percentage.
|
||||
func (h *ConflictHandler) applyProgressResolution(mediaItemID pgtype.UUID, userID pgtype.UUID, winnerSource string, data map[string]interface{}) error {
|
||||
func (h *ConflictHandler) applyProgressResolution(ctx context.Context, mediaItemID pgtype.UUID, userID pgtype.UUID, winnerSource string, data map[string]interface{}) error {
|
||||
req := wsync.SaveProgressRequest{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
@@ -361,12 +361,11 @@ func (h *ConflictHandler) applyProgressResolution(mediaItemID pgtype.UUID, userI
|
||||
req.TotalPages = &tp
|
||||
}
|
||||
|
||||
_, err := h.progressSvc.SaveProgress(context.Background(), req)
|
||||
_, err := h.progressSvc.SaveProgress(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ConflictHandler) applyAnnotationResolution(mediaItemID pgtype.UUID, userID pgtype.UUID, winnerData map[string]interface{}, conflictType string) error {
|
||||
ctx := context.Background()
|
||||
func (h *ConflictHandler) applyAnnotationResolution(ctx context.Context, mediaItemID pgtype.UUID, userID pgtype.UUID, winnerData map[string]interface{}, conflictType string) error {
|
||||
|
||||
dedupKey, _ := winnerData["dedup_key"].(string)
|
||||
if dedupKey == "" {
|
||||
@@ -702,7 +701,7 @@ func (h *ConflictHandler) BulkResolveConflicts(c *echo.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.applyProgressResolution(conflict.MediaItemID, conflict.UserID, winningSource, winnerData); err != nil {
|
||||
if err := h.applyProgressResolution(c.Request().Context(), conflict.MediaItemID, conflict.UserID, winningSource, winnerData); err != nil {
|
||||
results = append(results, ConflictResult{
|
||||
ConflictID: conflictIDStr,
|
||||
Status: "error",
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/rls"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -183,6 +184,13 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF
|
||||
c.Set("device_ctx", ctx)
|
||||
c.Set("device_id", device.ID.Bytes)
|
||||
|
||||
// RLS bridge (plan §5.1/A9): device-authenticated requests
|
||||
// (KOReader/Kobo sync) read media as the DEVICE OWNER — echo's
|
||||
// c.Set is echo-local, so the stdlib request context needs the
|
||||
// identity explicitly or every governed read raises.
|
||||
c.SetRequest(c.Request().WithContext(context.WithValue(
|
||||
c.Request().Context(), rls.CtxKeyUserID, device.UserID)))
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user