Report as revoked only what the read-back shows gone, and take back what the reader passed on (review of hq issue 337)
A reader given a privilege WITH GRANT OPTION that passed it on made the plain revoke fail with dependent privileges exist; the pass still listed the table as revoked and logged it every minute. The pass now retries such a change with CASCADE, which reaches only grants that came from the reader, reads the grants back, reports as revoked only what is gone, and says what is still held and why when anything is.
This commit is contained in:
@@ -35,6 +35,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -42,6 +43,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// ReadableTable is one entry of a contribution's `readable`.
|
||||
@@ -543,6 +546,40 @@ func (c *Client) LastReaderReport() ReaderReport {
|
||||
return c.reader.last
|
||||
}
|
||||
|
||||
// dependentPrivileges says whether the server refused a revoke because grants depend on it
|
||||
// (dependent_privileges_exist): the reader passed on a privilege it holds with grant option.
|
||||
func dependentPrivileges(err error) bool {
|
||||
var pg *pgconn.PgError
|
||||
return errors.As(err, &pg) && pg.Code == "2BP01"
|
||||
}
|
||||
|
||||
// cascaded is the change with every REVOKE made CASCADE.
|
||||
func cascaded(stmts []string) []string {
|
||||
out := make([]string, len(stmts))
|
||||
for i, s := range stmts {
|
||||
if strings.HasPrefix(s, "REVOKE ") {
|
||||
s += " CASCADE"
|
||||
}
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// gone is what was held before and is not held now.
|
||||
func gone(before, left []string) []string {
|
||||
still := map[string]bool{}
|
||||
for _, l := range left {
|
||||
still[l] = true
|
||||
}
|
||||
var out []string
|
||||
for _, b := range before {
|
||||
if !still[b] {
|
||||
out = append(out, b)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// readHeld is every privilege the reader holds in one database, table-level and column-level, each
|
||||
// with its grantor.
|
||||
func (c *Client) readHeld(ctx context.Context, db string) (held, error) {
|
||||
@@ -612,21 +649,34 @@ func (c *Client) reconcileDatabase(ctx context.Context, db string, decl []Readab
|
||||
}
|
||||
stmts, rep := planDatabase(decl, existing, h, noUsage)
|
||||
rep.Database, rep.Declared = db, decl != nil
|
||||
// What was revoked is what the read-back shows gone, never what was asked for.
|
||||
rep.Revoked = nil
|
||||
if len(stmts) > 0 {
|
||||
want := wantedColumns(decl, existing, &DatabaseReport{})
|
||||
before := unwanted(h, want)
|
||||
// One string: the simple protocol runs it as one implicit transaction, all or nothing.
|
||||
if _, err := c.Query(ctx, db, strings.Join(stmts, "; ")); err != nil {
|
||||
rep.Error = "changing the reader's grants: " + err.Error()
|
||||
return rep
|
||||
_, changeErr := c.Query(ctx, db, strings.Join(stmts, "; "))
|
||||
if changeErr != nil && dependentPrivileges(changeErr) {
|
||||
// The reader holds a grant option and passed the privilege on: a plain revoke refuses
|
||||
// while those grants stand. CASCADE takes them too — they all came from the reader, since
|
||||
// only grants depending on the reader's grant option depend on what is revoked here.
|
||||
_, changeErr = c.Query(ctx, db, strings.Join(cascaded(stmts), "; "))
|
||||
}
|
||||
// Read back: what the reader still holds beyond what is wanted is a failure, said as one —
|
||||
// never reported as revoked while it stands.
|
||||
after, err := c.readHeld(ctx, db)
|
||||
if err != nil {
|
||||
rep.Error = "reading the reader's grants back: " + err.Error()
|
||||
return rep
|
||||
}
|
||||
if left := unwanted(after, wantedColumns(decl, existing, &DatabaseReport{})); len(left) > 0 {
|
||||
rep.Error = "STILL HELD after revoking: the reader can still use " + strings.Join(left, ", ")
|
||||
left := unwanted(after, want)
|
||||
rep.Revoked = gone(before, left)
|
||||
if len(left) > 0 {
|
||||
why := "the revoke ran and they stand"
|
||||
if changeErr != nil {
|
||||
why = "changing the reader's grants failed: " + changeErr.Error()
|
||||
}
|
||||
rep.Error = "STILL HELD: the reader can still use " + strings.Join(left, ", ") + " (" + why + ")"
|
||||
} else if changeErr != nil {
|
||||
rep.Error = "changing the reader's grants: " + changeErr.Error()
|
||||
}
|
||||
}
|
||||
// Asked as the admin on the reader's behalf: whatever else gives the reader a secret (PUBLIC, a
|
||||
|
||||
Reference in New Issue
Block a user