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.
111 lines
4.7 KiB
Go
111 lines
4.7 KiB
Go
package main
|
|
|
|
// postgres's tools, and the mesh-store seat's verbs (novox/hq ADR 0159, 0160). The seat's verbs are
|
|
// named `mesh-store.<verb>`, so the runtime serves them as the seat's wherever this module holds it
|
|
// and never lists them as postgres's own; they are scoped to what the store enables — asking what it
|
|
// holds and reading from it — so retiring a database is postgres's tool and not the store's.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Seat is the store seat this module claims.
|
|
const Seat = "mesh-store"
|
|
|
|
var queryInput = map[string]any{
|
|
"database": map[string]any{"type": "string", "description": "the database to query"},
|
|
"sql": map[string]any{"type": "string", "description": "the SELECT (or other read-only) statement"},
|
|
}
|
|
|
|
func str(args map[string]any, key string) string {
|
|
if s, ok := args[key].(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func listTool(pg *Client, name, description string) stdio.Tool {
|
|
return stdio.Tool{Name: name, Description: description, Run: func(map[string]any) (any, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
dbs, err := pg.ListDatabases(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"databases": dbs}, nil
|
|
}}
|
|
}
|
|
|
|
func queryTool(pg *Client, name, prefix, description string) stdio.Tool {
|
|
return stdio.Tool{Name: name, Description: description, Input: queryInput, Run: func(args map[string]any) (any, error) {
|
|
database, sql := str(args, "database"), str(args, "sql")
|
|
if database == "" {
|
|
return nil, fmt.Errorf("%s: database is required", prefix)
|
|
}
|
|
if sql == "" {
|
|
return nil, fmt.Errorf("%s: sql is required", prefix)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
defer cancel()
|
|
r, err := pg.ReadOnlyQuery(ctx, database, sql)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"database": database, "command": r.Command, "rows": r.Maps()}, nil
|
|
}}
|
|
}
|
|
|
|
// Tools are postgres's own and the seat's verbs.
|
|
func Tools(pg *Client) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
listTool(pg, "postgres_list_databases", "List the databases on the postgres server, with their on-disk size."),
|
|
queryTool(pg, "postgres_query", "postgres_query",
|
|
"Run a read-only SQL query against a named database, as a login that changes nothing and reads only the "+
|
|
"columns the database's module declares readable — never a key, secret, token or password (hq issue 337). "+
|
|
"postgres_reader_grants says what it may read."),
|
|
{
|
|
Name: "postgres_reader_grants",
|
|
Description: "What the store's reader — the login every query runs as — may read in each database, as the last " +
|
|
"pass made it: the columns each module declares readable, what was refused because its name says it holds a " +
|
|
"secret, what was declared but is missing, what was revoked, and any secret column it can still read " +
|
|
"(exposed; queries there are then refused), and the SECURITY DEFINER functions and views it may use, which read " +
|
|
"with their owner's rights and which no grant shows. Names only, never values. (r)",
|
|
Run: func(map[string]any) (any, error) { return pg.LastReaderReport(), nil },
|
|
},
|
|
{
|
|
Name: "postgres_retire_database",
|
|
Description: "Take one database out of service on purpose: rename it to <name>_deleted_<date> and lock its owner's login. " +
|
|
"Nothing is dropped — the data stays on the server under the new name until a person removes it by hand. " +
|
|
"Repeat the database's name in confirm.",
|
|
Input: map[string]any{
|
|
"database": map[string]any{"type": "string", "description": "the database to retire"},
|
|
"confirm": map[string]any{"type": "string", "description": "the same name again, to say this is meant"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
database := str(args, "database")
|
|
if database == "" {
|
|
return nil, errors.New("postgres_retire_database: database is required")
|
|
}
|
|
if str(args, "confirm") != database {
|
|
return nil, errors.New("postgres_retire_database: confirm must repeat the database's name")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
aside, err := pg.RetireDatabase(ctx, database, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"database": database, "renamedTo": aside, "dropped": false}, nil
|
|
},
|
|
},
|
|
listTool(pg, Seat+".databases", "Every database the store holds, with its on-disk size."),
|
|
queryTool(pg, Seat+".query", "query", "One read-only statement against one database the store holds, reading only what that database's module "+
|
|
"declares readable and never a key, secret, token or password (hq issue 337)."),
|
|
}
|
|
}
|