Files
mesh-catalog/modules/postgres/cmd/postgres-provider/readable_live_test.go
T
jochen 8bf50f7555
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Revoke the reader's grants as their own grantors and read them back, alter its role only on drift, and report definer functions and views (review of hq issue 337)
A superuser's REVOKE acts as the table owner, so a grant to the reader made
by a role holding GRANT OPTION survived it while the report said revoked.
Each grant is now revoked under SET ROLE to its grantor, the grants are read
back, and anything still held is reported as a failure. The role was
altered every minute with its password in the statement, which a server
logging DDL would record and which could collide with a first query; it is
now altered only when an attribute, a setting or the password has drifted,
one ensure at a time. SECURITY DEFINER functions and views the reader may
use reach data without a grant on it, so every pass lists them. The README
says the name filter is a backstop and that the read-only session default
can be turned off by a statement.
2026-10-08 22:01:57 +02:00

184 lines
6.7 KiB
Go

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)
}
}
}