Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ All notable changes to backendkit are documented here. Format:
malformed `scope`/`scp` claim only matters with `RequireScopes` (it is then a 403): callers that do
not require scopes see no change.

### 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
Expand Down
5 changes: 3 additions & 2 deletions bff/pending_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <table> (
// state text PRIMARY KEY,
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions bff/pending_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading