package main // Against a real server (MESH_POSTGRES_LIVE), novox/hq issue 337: the reconcile gives the reader exactly // what is declared and no secret, takes back what is no longer declared, and a grant it does not own // that exposes a secret is found and refuses queries there. import ( "strings" "testing" ) func columnReadable(t *testing.T, c *Client, db, tbl, col string) bool { t.Helper() r, err := c.Query(ctx, db, "SELECT has_column_privilege("+Literal(Reader)+", "+Literal(tbl)+", "+Literal(col)+", 'SELECT')") if err != nil { t.Fatal(err) } return cell(r.Rows[0], 0) == "t" } // secretsReadable is the probe every database is held to: the columns the reader can read whose names // say they hold a secret, across every database the server holds. func secretsReadable(t *testing.T, c *Client) []string { t.Helper() dbs, err := c.ListDatabases(ctx) if err != nil { t.Fatal(err) } probe := strings.Replace(ExposureProbe, "pg_catalog.has_column_privilege(c.oid", "pg_catalog.has_column_privilege("+Literal(Reader)+", c.oid", 1) var out []string for _, db := range dbs { r, err := c.Query(ctx, db.Name, probe) if err != nil { t.Fatal(db.Name, err) } for _, e := range Exposed(r) { out = append(out, db.Name+": "+e) } } return out } func TestLiveTheReaderReadsWhatIsDeclaredAndNoSecret(t *testing.T) { c := liveClient(t) freshStore(t, c) defer dropStore(c) if got := secretsReadable(t, c); len(got) == 0 { t.Fatal("the install before the fix was not reproduced: no secret readable") } decl := Declarations{Tables: map[string][]ReadableTable{liveStore: { {Schema: "public", Table: "notes"}, {Schema: "public", Table: "vault"}, // every column: private is still refused, by its name {Schema: "audit", Table: "events"}, }}} rep := c.ReconcileReader(ctx, decl, "") if rep.Problem != "" { t.Fatal(rep.Problem) } if got := secretsReadable(t, c); len(got) != 0 { t.Fatalf("after the reconcile the reader can read %v", got) } for _, want := range [][3]string{{liveStore, "notes", "body"}, {liveStore, "vault", "name"}, {liveStore, "audit.events", "what"}} { if !columnReadable(t, c, want[0], want[1], want[2]) { t.Errorf("%v is declared and not readable", want) } } if columnReadable(t, c, liveStore, "vault", "private") || columnReadable(t, c, "identity", "signing_key", "public") { t.Fatal("a secret, or an undeclared database, is readable") } r, err := c.ReadOnlyQuery(ctx, liveStore, "SELECT name FROM vault") if err != nil || len(r.Rows) != 1 { t.Fatal("a declared column cannot be read:", r, err) } if _, err := c.ReadOnlyQuery(ctx, liveStore, "SELECT * FROM vault"); err == nil { t.Fatal("SELECT * read past the refused column") } if _, err := c.ReadOnlyQuery(ctx, "identity", "SELECT id FROM signing_key"); err == nil { t.Fatal("the signing key's table answered") } // The same declaration again changes nothing. again := c.ReconcileReader(ctx, decl, "") for _, db := range again.Databases { if len(db.Revoked) > 0 || db.Error != "" { t.Fatalf("a second pass changed %s: %+v", db.Database, db) } } // Narrowed: notes is no longer declared, and is taken back. decl.Tables[liveStore] = []ReadableTable{{Schema: "public", Table: "vault", Columns: []string{"name"}}} c.ReconcileReader(ctx, decl, "") if columnReadable(t, c, liveStore, "notes", "body") || columnReadable(t, c, liveStore, "audit.events", "what") { t.Fatal("a table no longer declared is still readable") } // A grant the provider does not own exposes a secret: found, said, and queries there refused. if _, err := c.Query(ctx, liveStore, "GRANT SELECT ON vault TO PUBLIC"); err != nil { t.Fatal(err) } rep = c.ReconcileReader(ctx, decl, "") var exposed []string for _, db := range rep.Databases { if db.Database == liveStore { exposed = db.Exposed } } if strings.Join(exposed, ",") != "public.vault.private" { t.Fatalf("exposure not found: %+v", rep.Databases) } if _, err := c.ReadOnlyQuery(ctx, liveStore, "SELECT name FROM vault"); err == nil || !strings.Contains(err.Error(), "public.vault.private") { t.Fatal("a query ran while the reader can read a secret:", err) } }