Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/integration/pertenant_isolation_integration_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 9bde0f9a74b89d01980fa658e5253acda7528d0c |
| Branch | per-tenant-routing |
| Path or archive member | internal/integration/pertenant_isolation_integration_test.go |
| Original SHA-256 | 4263aba4ea02b125e77a4dd3b1d8a0ecacb355f546ec8da7de4d392ce7da22eb |
| Git blob SHA | ba71d1ad05feb4b1bf2907e71e78b5db30eaa890 |
| Reading-copy SHA-256 | 4263aba4ea02b125e77a4dd3b1d8a0ecacb355f546ec8da7de4d392ce7da22eb |
Every recorded occurrence
These 3 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/integration/pertenant_isolation_integration_test.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/integration/pertenant_isolation_integration_test.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/integration/pertenant_isolation_integration_test.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
Recovered source transcript
Download complete source text ↓202 lines · 7.8 KB
//go:build integration
package integration
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/directory"
"github.com/missionworkforce/missionmatch/internal/platform/identity"
"github.com/missionworkforce/missionmatch/internal/platform/secrets"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
"github.com/missionworkforce/missionmatch/internal/platform/tenantpool"
)
// perTenantEnv holds the staging DSNs for the per_tenant validation. The test skips when they are unset.
//
// MM_CP_APP_DSN / MM_CP_MIGRATOR_DSN control plane (identity, directory)
// MM_TA_MIGRATOR_DSN / MM_TB_MIGRATOR_DSN the two tenant databases (for seeding)
// MM_TENANT_DSN_TENANT_ALPHA / _BETA the app DSNs the secrets resolver returns (the routing path)
type perTenantEnv struct {
cpApp, cpMig, taMig, tbMig string
}
func loadPerTenantEnv(t *testing.T) perTenantEnv {
t.Helper()
e := perTenantEnv{
cpApp: os.Getenv("MM_CP_APP_DSN"),
cpMig: os.Getenv("MM_CP_MIGRATOR_DSN"),
taMig: os.Getenv("MM_TA_MIGRATOR_DSN"),
tbMig: os.Getenv("MM_TB_MIGRATOR_DSN"),
}
if e.cpApp == "" || e.cpMig == "" || e.taMig == "" || e.tbMig == "" {
t.Skip("per_tenant DSNs not set (MM_CP_APP_DSN, MM_CP_MIGRATOR_DSN, MM_TA_MIGRATOR_DSN, MM_TB_MIGRATOR_DSN); skipping per_tenant validation")
}
if os.Getenv("MM_TENANT_DSN_TENANT_ALPHA") == "" || os.Getenv("MM_TENANT_DSN_TENANT_BETA") == "" {
t.Skip("per_tenant secret env DSNs not set (MM_TENANT_DSN_TENANT_ALPHA, MM_TENANT_DSN_TENANT_BETA)")
}
return e
}
func openPool(t *testing.T, dsn string) *pgxpool.Pool {
t.Helper()
p, err := pgxpool.New(context.Background(), dsn)
if err != nil {
t.Fatalf("pool %s: %v", dsn, err)
}
t.Cleanup(p.Close)
return p
}
// TestPerTenantPhysicalIsolation is the core proof of the database per tenant model: a request
// authenticated as tenant A, run through the REAL routing stack (the directory, the secrets resolver, and
// the bounded pool manager), reads only tenant A's rows, because tenant A's pool is bound to a DIFFERENT
// physical database than tenant B's. There is no cross database query path, so the isolation does not
// depend on RLS (RLS is verified separately, inside each tenant database, as defense in depth).
func TestPerTenantPhysicalIsolation(t *testing.T) {
env := loadPerTenantEnv(t)
ctx := context.Background()
cpMig := openPool(t, env.cpMig)
cpApp := openPool(t, env.cpApp)
taMig := openPool(t, env.taMig)
tbMig := openPool(t, env.tbMig)
orgA, orgB := uuid.New(), uuid.New()
userA, userB := uuid.New(), uuid.New()
// 1. Seed identity in the CONTROL PLANE: two orgs, an owner each, and the directory rows mapping each
// org to its tenant database (secret_ref is the env key the resolver maps to the tenant app DSN).
seedOrgUser(t, cpMig, orgA, userA, "Alpha Org", "alpha-"+orgA.String()[:8])
seedOrgUser(t, cpMig, orgB, userB, "Beta Org", "beta-"+orgB.String()[:8])
dir := directory.New(cpMig)
mustRegister(t, dir, orgA, "tenant_alpha", "tenant_alpha")
mustRegister(t, dir, orgB, "tenant_beta", "tenant_beta")
t.Cleanup(func() {
_, _ = cpMig.Exec(context.Background(), "DELETE FROM tenant_directory WHERE organization_id IN ($1,$2)", orgA, orgB)
})
// 2. Seed each tenant database: the org and owner (so business foreign keys resolve), then ONE distinct
// candidate per tenant. This is the onboarding identity seed (A5) plus the tenant's own data.
if _, err := identity.SeedTenant(ctx, cpMig, taMig, orgA); err != nil {
t.Fatalf("seed identity into tenant_alpha: %v", err)
}
if _, err := identity.SeedTenant(ctx, cpMig, tbMig, orgB); err != nil {
t.Fatalf("seed identity into tenant_beta: %v", err)
}
seedCandidateInto(t, taMig, orgA, "Alpha Analyst")
seedCandidateInto(t, tbMig, orgB, "Beta Builder")
// 3. Build the REAL routing stack the API uses in per_tenant mode: the env secrets resolver, the
// directory read by the app pool, and the bounded pool manager, wrapped as a routing tenant.DB.
sec, err := secrets.New("env", time.Minute)
if err != nil {
t.Fatalf("secrets: %v", err)
}
mgr := tenantpool.New(directory.New(cpApp), sec, tenantpool.Config{MaxPools: 8, MaxConnsPerPool: 4, IdleEvict: time.Minute})
t.Cleanup(mgr.Close)
routing := tenant.NewRouting(mgr)
// 4. The proof: tenant A's request reads only Alpha Analyst; tenant B's reads only Beta Builder.
aNames := listCandidateNames(t, routing, orgA)
bNames := listCandidateNames(t, routing, orgB)
if !contains(aNames, "Alpha Analyst") {
t.Fatalf("tenant A must see its own candidate, got %v", aNames)
}
if contains(aNames, "Beta Builder") {
t.Fatalf("ISOLATION BREACH: tenant A saw tenant B's candidate (%v)", aNames)
}
if !contains(bNames, "Beta Builder") {
t.Fatalf("tenant B must see its own candidate, got %v", bNames)
}
if contains(bNames, "Alpha Analyst") {
t.Fatalf("ISOLATION BREACH: tenant B saw tenant A's candidate (%v)", bNames)
}
t.Logf("physical isolation proven: A=%v (own database tenant_alpha) B=%v (own database tenant_beta)", aNames, bNames)
// 5. An unknown organization is a typed denial through the real manager, never a fallback.
if _, _, err := mgr.Acquire(ctx, uuid.New()); !errors.Is(err, directory.ErrUnknownTenant) {
t.Fatalf("an unknown org must be ErrUnknownTenant through the manager, got %v", err)
}
// 6. A request with no validated org reaches zero data (fail closed) even on the routing DB.
if err := routing.RunInTenantTx(context.Background(), func(context.Context, pgx.Tx) error { return nil }); !errors.Is(err, tenant.ErrNoTenant) {
t.Fatalf("a request with no validated org must fail closed, got %v", err)
}
}
func seedOrgUser(t *testing.T, cp *pgxpool.Pool, org, user uuid.UUID, name, slug string) {
t.Helper()
ctx := context.Background()
if _, err := cp.Exec(ctx, "INSERT INTO organizations (id, name, slug) VALUES ($1,$2,$3) ON CONFLICT (id) DO NOTHING", org, name, slug); err != nil {
t.Fatalf("seed org: %v", err)
}
if _, err := cp.Exec(ctx,
"INSERT INTO users (id, organization_id, email, full_name, role, status) VALUES ($1,$2,$3,$4,'owner','active') ON CONFLICT (organization_id, id) DO NOTHING",
user, org, slug+"@example.com", name+" Owner"); err != nil {
t.Fatalf("seed user: %v", err)
}
}
func mustRegister(t *testing.T, dir *directory.Directory, org uuid.UUID, dbName, secretRef string) {
t.Helper()
if err := dir.Register(context.Background(), directory.TenantConn{
OrganizationID: org, DBName: dbName, Host: "100.94.8.94", Port: 5432,
SecretRef: secretRef, Status: directory.StatusActive, Cloud: "local",
}); err != nil {
t.Fatalf("register %s: %v", dbName, err)
}
}
func seedCandidateInto(t *testing.T, pool *pgxpool.Pool, org uuid.UUID, name string) {
t.Helper()
if _, err := dbgen.New(pool).CreateCandidate(context.Background(), dbgen.CreateCandidateParams{
OrganizationID: org, FullName: name, ClearanceLevel: "secret", ClearancePoly: "none",
ClearanceStatus: "active", Available: true,
}); err != nil {
t.Fatalf("seed candidate %q: %v", name, err)
}
}
func listCandidateNames(t *testing.T, db *tenant.DB, org uuid.UUID) []string {
t.Helper()
var names []string
err := db.RunInTenantTx(tenant.WithOrg(context.Background(), org.String()), func(ctx context.Context, tx pgx.Tx) error {
rows, err := tx.Query(ctx, "SELECT full_name FROM candidates ORDER BY full_name")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var n string
if err := rows.Scan(&n); err != nil {
return err
}
names = append(names, n)
}
return rows.Err()
})
if err != nil {
t.Fatalf("list candidates for %s: %v", org, err)
}
return names
}
func contains(xs []string, s string) bool {
for _, x := range xs {
if x == s {
return true
}
}
return false
}