feat(store): video_connections table + RLS + connection store methods
Add the video_connections table (data-model VIDEO_CONNECTION) with FORCE row-level security keyed off tapir.current_user_id, identical to migration 003's per-user isolation pattern — connections are user-owned data and must be isolated at the DB layer, not only by application WHERE clauses. Store methods (UpsertConnection / ConnectionsForUser / DeleteConnection) all route through withUser so RLS scopes every access. UpsertConnection is idempotent on (user_id, provider). The OAuth refresh token never lives here; token_ref is the opaque SecretStore reference. Extend the RLS isolation proof to cover video_connections: seeded per user, included in the deny-all + scoped-read assertions, and added to the cross-user write-invisibility and survivor checks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import (
|
||||
// userIsolatedTables are the tables that carry a user_id and whose policy keys
|
||||
// directly off the tapir.current_user_id GUC.
|
||||
var userIsolatedTables = []string{
|
||||
"users", "videos", "transcripts", "summaries", "summary_actions",
|
||||
"users", "videos", "transcripts", "summaries", "summary_actions", "video_connections",
|
||||
}
|
||||
|
||||
// allIsolatedTables adds sink_deliveries, whose ownership is derived from its
|
||||
@@ -74,6 +74,11 @@ func seedUser(t *testing.T, p *pgxpool.Pool, userID string) seeded {
|
||||
VALUES ($1, 'store', 'delivered')`, summaryID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = p.Exec(ctx,
|
||||
`INSERT INTO video_connections (user_id, provider, token_ref, status)
|
||||
VALUES ($1, 'youtube', $2, 'active')`, userID, "youtube/"+userID+"/refresh_token")
|
||||
require.NoError(t, err)
|
||||
|
||||
return seeded{userID: userID, videoID: videoID, summaryID: summaryID}
|
||||
}
|
||||
|
||||
@@ -180,9 +185,11 @@ func TestRLSEnforcesPerUserIsolation(t *testing.T) {
|
||||
{"update summaries", `UPDATE summaries SET summary = 'hacked' WHERE user_id = $1`, b.userID},
|
||||
{"update summary_actions", `UPDATE summary_actions SET action = 'skipped' WHERE user_id = $1`, b.userID},
|
||||
{"update sink_deliveries", `UPDATE sink_deliveries SET status = 'hacked' WHERE summary_id = $1`, b.summaryID},
|
||||
{"update video_connections", `UPDATE video_connections SET token_ref = 'hacked' WHERE user_id = $1`, b.userID},
|
||||
{"delete summaries", `DELETE FROM summaries WHERE user_id = $1`, b.userID},
|
||||
{"delete summary_actions", `DELETE FROM summary_actions WHERE user_id = $1`, b.userID},
|
||||
{"delete sink_deliveries", `DELETE FROM sink_deliveries WHERE summary_id = $1`, b.summaryID},
|
||||
{"delete video_connections", `DELETE FROM video_connections WHERE user_id = $1`, b.userID},
|
||||
}
|
||||
for _, w := range writes {
|
||||
require.Equal(t, int64(0), scopedRowsAffected(t, app, userA, w.sql, w.arg),
|
||||
@@ -197,16 +204,19 @@ func TestRLSEnforcesPerUserIsolation(t *testing.T) {
|
||||
`SELECT summary FROM summaries WHERE user_id = $1`, b.userID).Scan(&bSummary))
|
||||
require.Equal(t, "sum", bSummary, "B's summary must be untouched by A's writes")
|
||||
|
||||
var bSummaries, bActions, bDeliveries int
|
||||
var bSummaries, bActions, bDeliveries, bConnections int
|
||||
require.NoError(t, super.QueryRow(ctx,
|
||||
`SELECT count(*) FROM summaries WHERE user_id = $1`, b.userID).Scan(&bSummaries))
|
||||
require.NoError(t, super.QueryRow(ctx,
|
||||
`SELECT count(*) FROM summary_actions WHERE user_id = $1`, b.userID).Scan(&bActions))
|
||||
require.NoError(t, super.QueryRow(ctx,
|
||||
fmt.Sprintf(`SELECT count(*) FROM sink_deliveries WHERE summary_id = '%s'`, b.summaryID)).Scan(&bDeliveries))
|
||||
require.NoError(t, super.QueryRow(ctx,
|
||||
`SELECT count(*) FROM video_connections WHERE user_id = $1 AND token_ref <> 'hacked'`, b.userID).Scan(&bConnections))
|
||||
require.Equal(t, 1, bSummaries, "A's DELETE must not have removed B's summary")
|
||||
require.Equal(t, 1, bActions, "A's DELETE must not have removed B's action")
|
||||
require.Equal(t, 1, bDeliveries, "A's DELETE must not have removed B's delivery")
|
||||
require.Equal(t, 1, bConnections, "A's writes must not have touched B's connection")
|
||||
|
||||
_ = a // a's ids are seeded for the symmetric read assertions above
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user