From 9ce293d68a56607694b210483949d2303aea4cf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 21:38:17 +0000 Subject: [PATCH] fix(bff): require UPDATE in the pending-login managed-schema check PostgresPendingLoginStore.Put is an INSERT ... ON CONFLICT DO UPDATE, which needs UPDATE on the table, but WithPendingLoginManagedSchema's start-up check verified only SELECT, INSERT and DELETE. A role granted exactly those passed the check and then failed every sign-in. The check now names UPDATE too, and the doc comment lists all four privileges. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKRxaeYxyDhmt42cehLsGA --- CHANGELOG.md | 8 ++++++++ bff/pending_login.go | 5 +++-- bff/pending_login_test.go | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77da10..ba08cfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to backendkit are documented here. Format: ## [Unreleased] +### Fixed +- **`WithPendingLoginManagedSchema` now also checks the `UPDATE` privilege.** `Put` is an + `INSERT … ON CONFLICT DO UPDATE`, which needs `UPDATE` on the table, but the start-up check + verified only `SELECT`, `INSERT` and `DELETE`. A role granted exactly those three passed the + check and then failed every sign-in. Such a role is now refused at start-up, naming `UPDATE`; + grant it `SELECT, INSERT, UPDATE, DELETE`, as for the session table. Found while moving the + monitoring console to the managed schema. + ## [1.25.0] - 2026-10-10 Minor release on the **v1** line: additive API only. `bff.PendingLoginStore` keeps pending logins diff --git a/bff/pending_login.go b/bff/pending_login.go index 3d526c3..f0cf69b 100644 --- a/bff/pending_login.go +++ b/bff/pending_login.go @@ -164,7 +164,8 @@ func WithPendingLoginErrorHandler(f func(op string, err error)) PendingLoginStor // WithPendingLoginManagedSchema tells NewPostgresPendingLoginStore that the // table is created by the caller's own migrations, as for // WithPostgresManagedSchema: it runs no DDL, checks the columns and that the -// role holds SELECT, INSERT and DELETE. The migration must create: +// role holds SELECT, INSERT, UPDATE and DELETE (Put is an INSERT … ON CONFLICT +// DO UPDATE, which needs UPDATE). The migration must create: // // CREATE TABLE ( // state text PRIMARY KEY, @@ -227,7 +228,7 @@ func NewPostgresPendingLoginStore(ctx context.Context, db *sql.DB, key []byte, t return nil, fmt.Errorf("bff: pending login store: %w", err) } if s.managed { - if err := checkTable(ctx, db, s.table, "state, data, created_at", "SELECT", "INSERT", "DELETE"); err != nil { + if err := checkTable(ctx, db, s.table, "state, data, created_at", "SELECT", "INSERT", "UPDATE", "DELETE"); err != nil { return nil, fmt.Errorf("bff: pending login store: managed table %s: %w", s.table, err) } return s, nil diff --git a/bff/pending_login_test.go b/bff/pending_login_test.go index de76479..d78e1a4 100644 --- a/bff/pending_login_test.go +++ b/bff/pending_login_test.go @@ -198,6 +198,29 @@ func TestPostgresPendingLoginStore_ManagedSchema(t *testing.T) { } } +// Put is an INSERT … ON CONFLICT DO UPDATE, so a role without UPDATE cannot +// store a login: the managed check must refuse it at start-up, naming the +// privilege, instead of letting every sign-in fail. +func TestPostgresPendingLoginStore_ManagedSchemaNeedsUpdate(t *testing.T) { + ctx := context.Background() + _, db := pgStore(t, time.Hour, time.Hour) + table := fmt.Sprintf("bff_test_pending_grant_%d", time.Now().UnixNano()) + t.Cleanup(func() { _, _ = db.Exec("DROP TABLE IF EXISTS " + table) }) + if _, err := db.Exec("CREATE TABLE " + table + " (state text PRIMARY KEY, data bytea NOT NULL, created_at timestamptz NOT NULL)"); err != nil { + t.Fatal(err) + } + store := db + if isSuperuser(t, db) { + store = limitedRoleDB(t, db, table, "SELECT, INSERT, DELETE") + } else if _, err := db.Exec("REVOKE UPDATE ON " + table + " FROM CURRENT_USER"); err != nil { + t.Fatal(err) + } + _, err := NewPostgresPendingLoginStore(ctx, store, testKey, 0, WithPendingLoginTable(table), WithPendingLoginManagedSchema()) + if err == nil || !strings.Contains(err.Error(), "UPDATE") { + t.Fatalf("missing UPDATE privilege: %v, want a start-up error naming it", err) + } +} + func TestNewPostgresPendingLoginStore_Validation(t *testing.T) { ctx := context.Background() if _, err := NewPostgresPendingLoginStore(ctx, nil, testKey, 0); err == nil {