Skip to content

security(#235): enforce ownership on recommendation_cache writes - #244

Merged
ChristopherRotnes merged 2 commits into
masterfrom
security/235-rec-cache-ownership
May 15, 2026
Merged

ChristopherRotnes merged 2 commits into
masterfrom
security/235-rec-cache-ownership

Conversation

@ChristopherRotnes

Copy link
Copy Markdown
Owner

Summary

  • Adds written_by uuid REFERENCES profiles(id) NOT NULL DEFAULT auth.uid() to recommendation_cache
  • Replaces the open UPDATE policy (any authenticated user) with owner-only: written_by = auth.uid()
  • Tightens INSERT policy to enforce written_by = auth.uid() so no user can write on behalf of another
  • Truncates the cache before migration (ephemeral data, 7-day TTL — safe to clear)
  • saveRecsCache in db.js requires no change: DEFAULT auth.uid() handles INSERT, and the conflict-UPDATE path preserves the existing written_by value
  • recsCacheCleanup Azure Function uses service role → bypasses RLS → unaffected

Tradeoff: If user A wrote a cache entry, user B's "Oppdater anbefalinger" refresh will silently fail to update the cache (B sees fresh recs in-session from the Claude call, but the shared cache keeps A's entry until it expires in 7 days). This is intentional — preventing poisoning takes priority over cross-user cache refresh.

Test plan

  • Open Report with any filter combination — recs load correctly (SELECT unaffected)
  • "Oppdater anbefalinger" button — fresh recs appear in-session even if cache write is skipped for a different writer
  • Verify via Supabase dashboard that written_by is populated on new cache entries
  • recsCacheCleanup still deletes expired + orphaned entries on Sunday

Closes #235

🤖 Generated with Claude Code

ChristopherRotnes and others added 2 commits May 15, 2026 22:48
…se JWT

The POST /api/sporty-sync HTTP trigger was authenticated via a shared
x-api-key checked against SPORTY_SYNC_API_KEY. While VITE_SPORTY_SYNC_API_KEY
was not in CI secrets, the pattern was fragile and the key could leak if
accidentally added. The /api/claude endpoint already handles this correctly
with verifySupabaseJwt + X-Supabase-Token; this commit applies the same
pattern to the sporty-sync trigger.

The /api/sporty-health monitoring endpoint retains x-api-key — it is only
called by external monitoring tools, never by the frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a written_by column (DEFAULT auth.uid(), NOT NULL) so each cache
entry is tied to the user who wrote it. The INSERT policy now requires
written_by = auth.uid(), and the UPDATE policy restricts refreshes to
the original writer — a different user's upsert on conflict is silently
skipped rather than overwriting the entry.

saveRecsCache in db.js requires no change: the DEFAULT column provides
written_by on INSERT automatically, and the existing value is preserved
on the conflict-UPDATE path. recsCacheCleanup uses service role and
bypasses RLS, so cleanup is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@supabase

supabase Bot commented May 15, 2026 •

Copy link
Copy Markdown

Updates to Preview Branch (security/235-rec-cache-ownership) ↗︎

Deployments Status Updated
Database ✅ Fri, 15 May 2026 20:54:14 UTC
Services ✅ Fri, 15 May 2026 20:54:14 UTC
APIs ✅ Fri, 15 May 2026 20:54:14 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations ✅ Fri, 15 May 2026 20:54:31 UTC
Migrations ❌ Fri, 15 May 2026 20:54:34 UTC
Seeding ⏸️ Fri, 15 May 2026 20:53:59 UTC
Edge Functions ⏸️ Fri, 15 May 2026 20:53:59 UTC

❌ Branch Error • Fri, 15 May 2026 20:54:35 UTC

ERROR: relation "session_templates" does not exist (SQLSTATE 42P01)
At statement: 0
-- Sprint 3: add template_type to session_templates
-- Nullable, no default — existing rows keep null (treated as unset in UI).
ALTER TABLE session_templates ADD COLUMN template_type text

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@ChristopherRotnes
ChristopherRotnes merged commit eb1b918 into master May 15, 2026
1 check failed
@ChristopherRotnes
ChristopherRotnes deleted the security/235-rec-cache-ownership branch May 15, 2026 20:59
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.

As a developer I want recommendation_cache writes to be ownership-enforced so I can prevent cache poisoning

1 participant