//go:build integration // Mission tag RLS isolation: a tenant candidate_mission_tag written by org A must be invisible to org B // and visible to org A, proving the WS7 tenant tag tables (migration 0027) inherit the same row level // security boundary as every other tenant table. Mission tags are never a scoring input and never reach // the eeo schema; this test only exercises the tenancy guarantee. package integration import ( "context" "testing" "github.com/jackc/pgx/v5" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" ) func TestCandidateMissionTagRLSIsolation(t *testing.T) { orgA, orgB := seedOrg(t), seedOrg(t) candA := seedCandidate(t, orgA, "secret", "active", true) // As org A, write a SIGINT tag for org A's candidate through the APP pool (RLS scoped write). if err := appDB.RunInTenantTx(ctxOrg(orgA), func(ctx context.Context, tx pgx.Tx) error { return dbgen.New(tx).UpsertCandidateMissionTag(ctx, dbgen.UpsertCandidateMissionTagParams{ OrganizationID: orgA, CandidateID: candA.ID, MissionCode: "SIGINT", Mechanism: "keyword", Evidence: "signals intelligence", }) }); err != nil { t.Fatalf("org A write candidate mission tag: %v", err) } // Org A lists candidates by the SIGINT mission and MUST see its own tagged candidate. var listedA []dbgen.Candidate if err := appDB.RunInTenantTx(ctxOrg(orgA), func(ctx context.Context, tx pgx.Tx) error { var e error listedA, e = dbgen.New(tx).ListCandidatesByMission(ctx, dbgen.ListCandidatesByMissionParams{ MissionCode: "SIGINT", RowLimit: 1000, }) return e }); err != nil { t.Fatalf("org A list by mission: %v", err) } sawA := false for _, c := range listedA { sawA = sawA || c.ID == candA.ID } if !sawA { t.Fatal("org A must see its own SIGINT tagged candidate") } // Org B lists candidates by the SAME mission and MUST NOT see org A's candidate (RLS hides the join). var listedB []dbgen.Candidate if err := appDB.RunInTenantTx(ctxOrg(orgB), func(ctx context.Context, tx pgx.Tx) error { var e error listedB, e = dbgen.New(tx).ListCandidatesByMission(ctx, dbgen.ListCandidatesByMissionParams{ MissionCode: "SIGINT", RowLimit: 1000, }) return e }); err != nil { t.Fatalf("org B list by mission: %v", err) } for _, c := range listedB { if c.ID == candA.ID { t.Fatal("ISOLATION BREACH: org B saw org A's SIGINT tagged candidate") } if c.OrganizationID != orgB { t.Fatalf("ISOLATION BREACH: org B saw a foreign tenant candidate %s", c.OrganizationID) } } // Belt and suspenders: a direct count of candidate_mission_tags under org B's session is zero, the // default deny shape every tenant table shares. var n int if err := appDB.RunInTenantTx(ctxOrg(orgB), func(ctx context.Context, tx pgx.Tx) error { return tx.QueryRow(ctx, "SELECT count(*) FROM candidate_mission_tags WHERE candidate_id = $1", candA.ID).Scan(&n) }); err != nil { t.Fatalf("org B count tags: %v", err) } if n != 0 { t.Fatalf("RLS breached: org B counted %d of org A's mission tags (want 0)", n) } // And under org A the same count is exactly one, proving the row is real, not silently dropped. if err := appDB.RunInTenantTx(ctxOrg(orgA), func(ctx context.Context, tx pgx.Tx) error { return tx.QueryRow(ctx, "SELECT count(*) FROM candidate_mission_tags WHERE candidate_id = $1", candA.ID).Scan(&n) }); err != nil { t.Fatalf("org A count tags: %v", err) } if n != 1 { t.Fatalf("org A should count exactly 1 of its own mission tags, got %d", n) } }