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) } } // A grant to the reader made by a role holding GRANT OPTION — not the owner — is taken back as that // role, and what still stands after a pass is a failure, not "revoked". SECURITY DEFINER functions and // views the reader may use are reported. func TestLiveAGrantByAnotherGrantorIsTakenBackAndDefinersAreSaid(t *testing.T) { c := liveClient(t) freshStore(t, c) defer dropStore(c) for _, sql := range []string{ "DROP ROLE IF EXISTS mesh_test_owner", "DROP ROLE IF EXISTS mesh_test_middle", } { if _, err := c.Query(ctx, "", sql); err != nil { t.Fatal(sql, err) } } defer func() { _, _ = c.Query(ctx, "", "DROP DATABASE IF EXISTS "+Ident(liveStore)+" WITH (FORCE)") _, _ = c.Query(ctx, "", "DROP ROLE IF EXISTS mesh_test_middle") _, _ = c.Query(ctx, "", "DROP ROLE IF EXISTS mesh_test_owner") }() for _, sql := range []string{ "CREATE ROLE mesh_test_owner", "CREATE ROLE mesh_test_middle", } { if _, err := c.Query(ctx, "", sql); err != nil { t.Fatal(sql, err) } } for _, sql := range []string{ "ALTER TABLE notes OWNER TO mesh_test_owner", "GRANT SELECT ON notes TO mesh_test_middle WITH GRANT OPTION", "SET ROLE mesh_test_middle; GRANT SELECT ON notes TO " + Ident(Reader) + "; GRANT SELECT (body) ON notes TO " + Ident(Reader) + "; RESET ROLE", "CREATE FUNCTION peek() RETURNS bytea LANGUAGE sql SECURITY DEFINER AS 'SELECT private FROM vault LIMIT 1'", "CREATE VIEW plain_names AS SELECT name FROM vault", "GRANT SELECT ON plain_names TO " + Ident(Reader), } { if _, err := c.Query(ctx, liveStore, sql); err != nil { t.Fatal(sql, err) } } var mine DatabaseReport for _, db := range c.ReconcileReader(ctx, Declarations{}, "").Databases { if db.Database == liveStore { mine = db } } if mine.Error != "" { t.Fatal("left standing:", mine.Error) } if columnReadable(t, c, liveStore, "notes", "body") { t.Fatal("the other grantor's grant survived") } if strings.Join(mine.Definers, ",") != "public.peek()" || strings.Join(mine.Views, ",") != "" { t.Fatalf("definers %v, views %v", mine.Definers, mine.Views) } // A second pass finds nothing to take back. for _, db := range c.ReconcileReader(ctx, Declarations{}, "").Databases { if db.Database == liveStore && (len(db.Revoked) > 0 || db.Error != "") { t.Fatalf("%+v", db) } } // A view granted to PUBLIC is said. if _, err := c.Query(ctx, liveStore, "GRANT SELECT ON plain_names TO PUBLIC"); err != nil { t.Fatal(err) } for _, db := range c.ReconcileReader(ctx, Declarations{}, "").Databases { if db.Database == liveStore && strings.Join(db.Views, ",") != "public.plain_names" { t.Fatalf("views %v", db.Views) } } } // The reader was given a table WITH GRANT OPTION and passed it on. A plain revoke then fails with // "dependent privileges exist" and changes nothing; the pass must take back the reader's grant and // what the reader passed on (CASCADE reaches only grants that came from the reader), report as // revoked only what the read-back shows gone, and say what is still held and why if anything is. func TestLiveAGrantTheReaderPassedOnIsTakenBack(t *testing.T) { c := liveClient(t) freshStore(t, c) defer dropStore(c) defer func() { _, _ = c.Query(ctx, "", "DROP ROLE IF EXISTS mesh_test_onward") }() if _, err := c.Query(ctx, "", "DROP ROLE IF EXISTS mesh_test_onward; CREATE ROLE mesh_test_onward"); err != nil { t.Fatal(err) } for _, sql := range []string{ "REVOKE ALL ON vault FROM " + Ident(Reader), "GRANT SELECT ON notes TO " + Ident(Reader) + " WITH GRANT OPTION", "SET ROLE " + Ident(Reader) + "; GRANT SELECT ON notes TO mesh_test_onward; RESET ROLE", } { if _, err := c.Query(ctx, liveStore, sql); err != nil { t.Fatal(sql, err) } } var mine DatabaseReport for _, db := range c.ReconcileReader(ctx, Declarations{}, "").Databases { if db.Database == liveStore { mine = db } } if mine.Error != "" { t.Fatal("left standing:", mine.Error) } if columnReadable(t, c, liveStore, "notes", "body") { t.Fatalf("the reader still reads notes, and the pass said revoked %v", mine.Revoked) } if strings.Join(mine.Revoked, ",") != "public.notes" { t.Fatalf("revoked %v", mine.Revoked) } r, err := c.Query(ctx, liveStore, "SELECT has_table_privilege('mesh_test_onward', 'notes', 'SELECT')") if err != nil || cell(r.Rows[0], 0) != "f" { t.Fatal("what the reader passed on survived:", r.Rows, err) } // The next pass finds nothing held, and says nothing revoked. for _, db := range c.ReconcileReader(ctx, Declarations{}, "").Databases { if db.Database == liveStore && (len(db.Revoked) > 0 || db.Error != "") { t.Fatalf("%+v", db) } } }