mesh_store_reader held pg_read_all_data, so every caller of mesh-store.query and postgres_query could read every column the store holds: the signing key nodes trust, sealed secrets, password hashes. The reader now holds no role; each minute, on every database, its memberships are revoked and its grants brought to what the consumer's contribution lists under readable, minus any column or table whose name says it holds a secret. Before a statement runs, the reader's own session is asked which secret columns it can read and the statement is refused if any; a statement naming a known secret table is refused before it is sent.
116 lines
4.0 KiB
Go
116 lines
4.0 KiB
Go
package main
|
|
|
|
// Against a real server, when one is named (MESH_POSTGRES_LIVE, as live_test.go) — skipped otherwise.
|
|
//
|
|
// novox/hq issue 337: an install made before it gave the store's reader `pg_read_all_data`, and with it
|
|
// every secret the store holds. This proves, through nothing but the query the store seat serves, that
|
|
// such an install's reader reads no secret any more: the first query takes the role back, and a secret
|
|
// the reader can still read (a grant by hand) refuses the query rather than answering it. It uses only
|
|
// what the module had before the fix, so it fails on the commit before it.
|
|
|
|
import (
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const liveStore = "mesh_test_store"
|
|
|
|
func liveClient(t *testing.T) *Client {
|
|
t.Helper()
|
|
raw := os.Getenv("MESH_POSTGRES_LIVE")
|
|
if raw == "" {
|
|
t.Skip("MESH_POSTGRES_LIVE names no server")
|
|
}
|
|
u, _ := url.Parse(raw)
|
|
pw, _ := u.User.Password()
|
|
env := map[string]string{"MESH_PROVISION_POSTGRES": raw, "MESH_POSTGRES_PASSWORD": pw, "MESH_POSTGRES_READER_PASSWORD": "reader-pw"}
|
|
c, err := ClientFromEnv(func(k string) string { return env[k] })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// freshStore makes a database holding a key beside a plain table, and a reader as an install before
|
|
// issue 337 left it: granted pg_read_all_data, and a table granted to it by hand.
|
|
func freshStore(t *testing.T, c *Client) {
|
|
t.Helper()
|
|
for _, db := range []string{liveStore, "identity"} {
|
|
if _, err := c.Query(ctx, "", "DROP DATABASE IF EXISTS "+Ident(db)+" WITH (FORCE)"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := c.Query(ctx, "", "CREATE DATABASE "+Ident(db)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
setup := []string{
|
|
"CREATE TABLE vault (id int, name text, private bytea)",
|
|
"INSERT INTO vault VALUES (1, 'the mesh', 'not to be read')",
|
|
"CREATE TABLE notes (id int, body text)",
|
|
"INSERT INTO notes VALUES (1, 'plain')",
|
|
"CREATE SCHEMA audit",
|
|
"CREATE TABLE audit.events (at timestamptz default now(), what text)",
|
|
"INSERT INTO audit.events (what) VALUES ('started')",
|
|
}
|
|
for _, sql := range setup {
|
|
if _, err := c.Query(ctx, liveStore, sql); err != nil {
|
|
t.Fatal(sql, err)
|
|
}
|
|
}
|
|
if _, err := c.Query(ctx, "identity", "CREATE TABLE signing_key (id int, public bytea, private bytea); INSERT INTO signing_key VALUES (1, 'p', 'k')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
has, err := c.exists(ctx, "SELECT 1 FROM pg_roles WHERE rolname = "+Literal(Reader))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
verb := "CREATE"
|
|
if has {
|
|
verb = "ALTER"
|
|
}
|
|
for _, sql := range []string{
|
|
verb + " ROLE " + Ident(Reader) + " LOGIN PASSWORD 'reader-pw'",
|
|
"GRANT pg_read_all_data TO " + Ident(Reader),
|
|
} {
|
|
if _, err := c.Query(ctx, "", sql); err != nil {
|
|
t.Fatal(sql, err)
|
|
}
|
|
}
|
|
if _, err := c.Query(ctx, liveStore, "GRANT SELECT ON vault TO "+Ident(Reader)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func dropStore(c *Client) {
|
|
for _, db := range []string{liveStore, "identity"} {
|
|
_, _ = c.Query(ctx, "", "DROP DATABASE IF EXISTS "+Ident(db)+" WITH (FORCE)")
|
|
}
|
|
}
|
|
|
|
func TestLiveAnInstallsReaderReadsNoSecret(t *testing.T) {
|
|
c := liveClient(t)
|
|
freshStore(t, c)
|
|
defer dropStore(c)
|
|
|
|
// The signing key, by name, in the controller's database: refused.
|
|
if r, err := c.ReadOnlyQuery(ctx, "identity", "SELECT private FROM signing_key"); err == nil {
|
|
t.Fatalf("the reader read the signing key: %v", r.Maps())
|
|
}
|
|
// A plain column of a table holding a key, granted to the reader by hand: refused while the reader
|
|
// can read the key beside it, and the key named.
|
|
r, err := c.ReadOnlyQuery(ctx, liveStore, "SELECT name FROM vault")
|
|
if err == nil {
|
|
t.Fatalf("answered while the reader can read vault.private: %v", r.Maps())
|
|
}
|
|
if !strings.Contains(err.Error(), "public.vault.private") {
|
|
t.Fatal(err)
|
|
}
|
|
// The role that read everything is taken back by the first query.
|
|
r, err = c.Query(ctx, "", "SELECT pg_has_role("+Literal(Reader)+", 'pg_read_all_data', 'MEMBER') AS reads_all")
|
|
if err != nil || cell(r.Rows[0], 0) != "f" {
|
|
t.Fatalf("the reader still holds pg_read_all_data: %v %v", r.Rows, err)
|
|
}
|
|
}
|