Verify Forgejo PostgreSQL operations and preserve tracing hook context
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
This commit is contained in:
parent
3fb031a47f
commit
308e83fcfb
4 changed files with 521 additions and 9 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Forgejo release preparation (build only)
|
||||
|
||||
The Apps Makefile targets package the digest-bound 37-path Forgejo 11.0.3
|
||||
The Apps Makefile targets package the digest-bound 40-path Forgejo 11.0.3
|
||||
source candidate in `forgejo-security-extension.patch`. It combines the original
|
||||
three-group any-of gate, private secret-file input, private source-operation DB
|
||||
primitives and the producer-owned Access Engine client in the OIDC callback.
|
||||
|
|
@ -108,6 +108,29 @@ prove an actual browser, factor, custody or native rollout.
|
|||
|
||||
The current integration covers this guarded OIDC callback. Ongoing application
|
||||
HTTP/API/Git/SSH paths, current operation/inverse sessions, durable private
|
||||
evidence and retention, PostgreSQL/native proof and exclusion of all release
|
||||
evidence and retention, deployed PostgreSQL/custody acceptance and exclusion of all release
|
||||
writers remain open in RAPPS-WP-0016-T08/T09 and NK-WP-0048-T43. An old image
|
||||
approval or source test result cannot admit this candidate.
|
||||
|
||||
## PostgreSQL operation-source verification
|
||||
|
||||
The source primitive has now run on a disposable PostgreSQL 16.14 instance using
|
||||
the actual Forgejo engine, model schemas and OAuth configuration types. Tests
|
||||
cover default, explicit-public and named schemas, exact source restoration, drift
|
||||
and linked-account refusals, trigger rollback, uncooperative SQL writer exclusion
|
||||
and concurrent preparation. Independent client processes and an actual server
|
||||
restart verify prepared/applied/inverse journal recovery. The test-only
|
||||
`source_operation_postgres` tag selects the integration tests; no runtime route,
|
||||
credential, session or authority callback is added. The companion Custodian
|
||||
`forgejo-postgres-source-r1` packet holds the isolated fixture, commands, source
|
||||
pins, counterexamples and original failed attempts.
|
||||
|
||||
Actual PostgreSQL diagnostics exposed a tracing-hook composition crash. Tracing
|
||||
now publishes its task context to the shared XORM hook context so later hooks
|
||||
retain it. Ordinary and private SQLite traces and actual PostgreSQL failure
|
||||
queries exercise this path. Controlled copies without the table lock or private
|
||||
query marker fail their respective regressions. These are source/database
|
||||
mechanics checks; owner admission, current authenticated operation/inverse
|
||||
sessions, runtime/backup custody, retention, every release writer and native
|
||||
deployed-database acceptance remain separate obligations. The previous frozen
|
||||
release is historical and does not contain this context correction.
|
||||
|
|
|
|||
|
|
@ -922,6 +922,388 @@
|
|||
+ }
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/models/auth/source_operation_postgres_export_test.go
|
||||
@@ -0,0 +1,7 @@
|
||||
+//go:build source_operation_postgres
|
||||
+
|
||||
+package auth
|
||||
+
|
||||
+// Test-only seam: the production transaction body still obtains its real locks
|
||||
+// and private query context. No runtime API or authority callback is added.
|
||||
+var SourceOperationPostgresTransactionForTest = sourceOperationTx
|
||||
--- /dev/null
|
||||
+++ b/models/auth/source_operation_postgres_test.go
|
||||
@@ -0,0 +1,369 @@
|
||||
+//go:build source_operation_postgres
|
||||
+
|
||||
+package auth_test
|
||||
+
|
||||
+import (
|
||||
+ "bytes"
|
||||
+ "context"
|
||||
+ "errors"
|
||||
+ "fmt"
|
||||
+ "os"
|
||||
+ "runtime/trace"
|
||||
+ "strings"
|
||||
+ "sync"
|
||||
+ "testing"
|
||||
+ "time"
|
||||
+
|
||||
+ auth "forgejo.org/models/auth"
|
||||
+ "forgejo.org/models/db"
|
||||
+ user_model "forgejo.org/models/user"
|
||||
+ "forgejo.org/modules/log"
|
||||
+ "forgejo.org/modules/setting"
|
||||
+ oauth2 "forgejo.org/services/auth/source/oauth2"
|
||||
+
|
||||
+ "github.com/lib/pq"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+ "xorm.io/xorm"
|
||||
+)
|
||||
+
|
||||
+type postgresOperationLog struct {
|
||||
+ log.Logger
|
||||
+ mu sync.Mutex
|
||||
+ entries []string
|
||||
+}
|
||||
+
|
||||
+func (l *postgresOperationLog) Log(_ int, _ log.Level, format string, values ...any) {
|
||||
+ l.mu.Lock()
|
||||
+ defer l.mu.Unlock()
|
||||
+ l.entries = append(l.entries, fmt.Sprintf(format, values...))
|
||||
+}
|
||||
+func (l *postgresOperationLog) take() string {
|
||||
+ l.mu.Lock()
|
||||
+ defer l.mu.Unlock()
|
||||
+ result := strings.Join(l.entries, "\n")
|
||||
+ l.entries = nil
|
||||
+ return result
|
||||
+}
|
||||
+
|
||||
+// The real Forgejo engine/driver, registered model schemas and public operation
|
||||
+// functions run. Only the database, application configuration and actors are
|
||||
+// synthetic. No deployed owner database or authenticated operation is claimed.
|
||||
+func postgresOperationDB(t *testing.T, schema string) (context.Context, *xorm.Engine, *xorm.Engine) {
|
||||
+ t.Helper()
|
||||
+ require.Equal(t, "/run/source-operation-postgres", os.Getenv("CUSTODIAN_PG_SOCKET"), "explicit isolated socket required")
|
||||
+ oldSettings, oldContext := setting.Database, db.DefaultContext
|
||||
+ oldEngine, ok := db.GetEngine(oldContext).(*xorm.Engine)
|
||||
+ require.True(t, ok)
|
||||
+ expectedSchema := schema
|
||||
+ if expectedSchema == "" {
|
||||
+ expectedSchema = "public"
|
||||
+ }
|
||||
+ raw, err := xorm.NewEngine("postgres", "host=/run/source-operation-postgres user=custodian_test dbname=custodian_source sslmode=disable search_path="+expectedSchema)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = raw.Exec(`CREATE SCHEMA IF NOT EXISTS "` + expectedSchema + `"`)
|
||||
+ require.NoError(t, err)
|
||||
+ setting.Database.Type = "postgres"
|
||||
+ setting.Database.Host = "/run/source-operation-postgres"
|
||||
+ setting.Database.Name = "custodian_source"
|
||||
+ setting.Database.User = "custodian_test"
|
||||
+ setting.Database.Passwd = ""
|
||||
+ setting.Database.Schema = schema
|
||||
+ setting.Database.SSLMode = "disable"
|
||||
+ setting.Database.LogSQL = false
|
||||
+ setting.Database.MaxOpenConns, setting.Database.MaxIdleConns = 8, 2
|
||||
+ setting.Database.SlowQueryThreshold = 0
|
||||
+ require.NoError(t, db.InitEngine(context.Background()))
|
||||
+ engine, ok := db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||
+ require.True(t, ok)
|
||||
+ t.Cleanup(func() {
|
||||
+ _ = engine.Close()
|
||||
+ _ = raw.Close()
|
||||
+ setting.Database = oldSettings
|
||||
+ db.SetDefaultEngine(oldContext, oldEngine)
|
||||
+ })
|
||||
+ version, err := engine.Query("SELECT current_setting('server_version_num')::integer / 10000 AS major, current_schema() AS schema")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "16", string(version[0]["major"]))
|
||||
+ require.Equal(t, expectedSchema, string(version[0]["schema"]))
|
||||
+ require.NoError(t, db.SyncAllTables())
|
||||
+ auth.RegisterTypeConfig(auth.OAuth2, new(oauth2.Source))
|
||||
+ return db.DefaultContext, engine, raw
|
||||
+}
|
||||
+
|
||||
+func resetPostgresOperations(t *testing.T, engine *xorm.Engine) {
|
||||
+ t.Helper()
|
||||
+ _, err := engine.Exec(`DROP TRIGGER IF EXISTS custodian_source_rewrite ON login_source`)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = engine.Exec(`TRUNCATE login_source_operation, login_source, "user", external_login_user RESTART IDENTITY CASCADE`)
|
||||
+ require.NoError(t, err)
|
||||
+}
|
||||
+
|
||||
+func TestSourceOperationPostgres(t *testing.T) {
|
||||
+ for _, profile := range []struct{ name, schema string }{{"default", ""}, {"public", "public"}, {"named", "custodian_named"}} {
|
||||
+ t.Run(profile.name, func(t *testing.T) {
|
||||
+ ctx, engine, raw := postgresOperationDB(t, profile.schema)
|
||||
+ run := func(name string, f func(*testing.T)) {
|
||||
+ t.Run(name, func(t *testing.T) { resetPostgresOperations(t, engine); f(t) })
|
||||
+ }
|
||||
+ run("create-reconcile-inverse", func(t *testing.T) {
|
||||
+ before := operationRows(t, ctx)
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-create-operation", "postgres-create-case", 0, operationSource())
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ r, err := auth.ApplySourceOperation(ctx, "postgres-create-operation", "postgres-create-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "applied", r.Phase)
|
||||
+ require.Positive(t, r.SourceID)
|
||||
+ post := operationRows(t, ctx)
|
||||
+ again, err := auth.ApplySourceOperation(ctx, "postgres-create-operation", "postgres-create-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, r, again)
|
||||
+ require.Equal(t, post, operationRows(t, ctx))
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-create-operation", "postgres-wrong-case")
|
||||
+ require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
+ r, err = auth.ReconcileSourceOperation(ctx, "postgres-create-operation", "postgres-create-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "applied", r.Phase)
|
||||
+ r, err = auth.InverseSourceOperation(ctx, "postgres-create-operation", "postgres-create-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "reverted", r.Phase)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-create-operation", "postgres-create-case")
|
||||
+ require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
+ })
|
||||
+ run("exact-raw-preexisting-restoration", func(t *testing.T) {
|
||||
+ s := operationSource()
|
||||
+ require.NoError(t, auth.CreateSource(ctx, s))
|
||||
+ _, err := engine.Exec(`UPDATE login_source SET cfg = ? WHERE id = ?`, `{"Provider":"openidConnect","ClientID":"synthetic-client","ClientSecret":"synthetic-private-original","FutureGuard":{"x":true}}`, s.ID)
|
||||
+ require.NoError(t, err)
|
||||
+ before := operationRows(t, ctx)
|
||||
+ s.Cfg.(*oauth2.Source).ClientSecret = "synthetic-private-new"
|
||||
+ _, err = auth.PrepareSourceOperation(ctx, "postgres-restore-operation", "postgres-restore-case", s.ID, s)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-restore-operation", "postgres-restore-case")
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-restore-operation", "postgres-restore-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ })
|
||||
+ run("unchanged-preexisting-survives", func(t *testing.T) {
|
||||
+ s := operationSource()
|
||||
+ require.NoError(t, auth.CreateSource(ctx, s))
|
||||
+ before := operationRows(t, ctx)
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-unchanged-operation", "postgres-unchanged-case", s.ID, s)
|
||||
+ require.NoError(t, err)
|
||||
+ r, err := auth.ApplySourceOperation(ctx, "postgres-unchanged-operation", "postgres-unchanged-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "unchanged", r.Phase)
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-unchanged-operation", "postgres-unchanged-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ })
|
||||
+ for _, stage := range []string{"before-apply", "before-inverse"} {
|
||||
+ run("private-drift-"+stage, func(t *testing.T) {
|
||||
+ s := operationSource()
|
||||
+ require.NoError(t, auth.CreateSource(ctx, s))
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-drift-operation", "postgres-drift-case", s.ID, s)
|
||||
+ require.NoError(t, err)
|
||||
+ if stage == "before-inverse" {
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-drift-operation", "postgres-drift-case")
|
||||
+ require.NoError(t, err)
|
||||
+ }
|
||||
+ _, err = engine.Exec(`UPDATE login_source SET cfg = cfg || ' ' WHERE id = ?`, s.ID)
|
||||
+ require.NoError(t, err)
|
||||
+ before := operationRows(t, ctx)
|
||||
+ if stage == "before-apply" {
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-drift-operation", "postgres-drift-case")
|
||||
+ } else {
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-drift-operation", "postgres-drift-case")
|
||||
+ }
|
||||
+ require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ })
|
||||
+ }
|
||||
+ for _, kind := range []string{"user", "external"} {
|
||||
+ run("linked-"+kind+"-refuses-deletion", func(t *testing.T) {
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-linked-operation", "postgres-linked-case", 0, operationSource())
|
||||
+ require.NoError(t, err)
|
||||
+ r, err := auth.ApplySourceOperation(ctx, "postgres-linked-operation", "postgres-linked-case")
|
||||
+ require.NoError(t, err)
|
||||
+ if kind == "user" {
|
||||
+ _, err = engine.Insert(&user_model.User{ID: 1, LowerName: "synthetic", Name: "synthetic", Email: "synthetic@invalid.test", LoginSource: r.SourceID})
|
||||
+ } else {
|
||||
+ _, err = engine.Exec(`INSERT INTO external_login_user (external_id, user_id, login_source_id) VALUES ('synthetic-external', 1, ?)`, r.SourceID)
|
||||
+ }
|
||||
+ require.NoError(t, err)
|
||||
+ before := operationRows(t, ctx)
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-linked-operation", "postgres-linked-case")
|
||||
+ require.True(t, auth.IsErrSourceInUse(err))
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ var count int64
|
||||
+ if kind == "user" {
|
||||
+ count, err = engine.Table("user").Where("login_source = ?", r.SourceID).Count()
|
||||
+ } else {
|
||||
+ count, err = engine.Table("external_login_user").Where("login_source_id = ?", r.SourceID).Count()
|
||||
+ }
|
||||
+ require.NoError(t, err)
|
||||
+ require.EqualValues(t, 1, count)
|
||||
+ })
|
||||
+ }
|
||||
+ run("unexpected-trigger-post-state-rolls-back", func(t *testing.T) {
|
||||
+ before := operationRows(t, ctx)
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-trigger-operation", "postgres-trigger-case", 0, operationSource())
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = engine.Exec(`CREATE OR REPLACE FUNCTION custodian_rewrite_source() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.name := 'unexpected-rewrite'; RETURN NEW; END $$`)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = engine.Exec(`CREATE TRIGGER custodian_source_rewrite BEFORE INSERT ON login_source FOR EACH ROW EXECUTE FUNCTION custodian_rewrite_source()`)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-trigger-operation", "postgres-trigger-case")
|
||||
+ require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ r, err := auth.ReconcileSourceOperation(ctx, "postgres-trigger-operation", "postgres-trigger-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "prepared", r.Phase)
|
||||
+ })
|
||||
+ run("driver-error-rollback-and-private-diagnostics", func(t *testing.T) {
|
||||
+ captured := new(postgresOperationLog)
|
||||
+ engine.AddHook(&db.SlowQueryHook{Logger: captured})
|
||||
+ engine.AddHook(&db.ErrorQueryHook{Logger: captured})
|
||||
+ const control = "synthetic-postgres-unmarked-control"
|
||||
+ _, err := db.GetEngine(ctx).Exec("SELECT missing_synthetic_function(?)", control)
|
||||
+ require.Error(t, err)
|
||||
+ require.Contains(t, captured.take(), control, "actual unmarked diagnostic control must be observable")
|
||||
+ _, err = engine.Exec(`CREATE OR REPLACE FUNCTION custodian_fail_source() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION '%', NEW.cfg; END $$`)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = engine.Exec(`CREATE TRIGGER custodian_source_rewrite BEFORE INSERT ON login_source FOR EACH ROW EXECUTE FUNCTION custodian_fail_source()`)
|
||||
+ require.NoError(t, err)
|
||||
+ before := operationRows(t, ctx)
|
||||
+ _, err = auth.PrepareSourceOperation(ctx, "postgres-error-operation", "postgres-error-case", 0, operationSource())
|
||||
+ require.NoError(t, err)
|
||||
+ captured.take()
|
||||
+ var tracing bytes.Buffer
|
||||
+ require.NoError(t, trace.Start(&tracing))
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-error-operation", "postgres-error-case")
|
||||
+ trace.Stop()
|
||||
+ require.ErrorIs(t, err, auth.ErrSourceOperationStorage)
|
||||
+ messages := captured.take()
|
||||
+ require.Contains(t, messages, "[Error SQL Query] [owner-confined query]")
|
||||
+ require.Contains(t, messages, "[Slow SQL Query] [owner-confined query]")
|
||||
+ require.NotContains(t, messages, "synthetic-private-original")
|
||||
+ require.False(t, bytes.Contains(tracing.Bytes(), []byte("synthetic-private-original")), "actual PostgreSQL trace disclosed private cfg")
|
||||
+ require.Equal(t, before, operationRows(t, ctx))
|
||||
+ r, err := auth.ReconcileSourceOperation(ctx, "postgres-error-operation", "postgres-error-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "prepared", r.Phase)
|
||||
+ })
|
||||
+ run("uncooperative-writers-excluded", func(t *testing.T) {
|
||||
+ locked, release, done := make(chan struct{}), make(chan struct{}), make(chan error, 1)
|
||||
+ var once sync.Once
|
||||
+ releaseLock := func() { once.Do(func() { close(release) }) }
|
||||
+ defer releaseLock()
|
||||
+ go func() {
|
||||
+ done <- auth.SourceOperationPostgresTransactionForTest(ctx, func(context.Context) error { close(locked); <-release; return nil })
|
||||
+ }()
|
||||
+ select {
|
||||
+ case <-locked:
|
||||
+ case err := <-done:
|
||||
+ t.Fatalf("lock acquisition refused: %v", err)
|
||||
+ case <-time.After(5 * time.Second):
|
||||
+ t.Fatal("lock acquisition timed out")
|
||||
+ }
|
||||
+ statements := []string{`UPDATE login_source_operation SET phase=phase WHERE operation_id='absent'`, `UPDATE login_source SET name=name WHERE id=-1`, `UPDATE "user" SET login_source=login_source WHERE id=-1`, `UPDATE external_login_user SET login_source_id=login_source_id WHERE user_id=-1`}
|
||||
+ for _, statement := range statements {
|
||||
+ session := raw.NewSession()
|
||||
+ require.NoError(t, session.Begin())
|
||||
+ _, err := session.Exec(`SET LOCAL lock_timeout='150ms'`)
|
||||
+ require.NoError(t, err)
|
||||
+ _, err = session.Exec(statement)
|
||||
+ var pgerr *pq.Error
|
||||
+ blocked := errors.As(err, &pgerr) && pgerr.Code == "55P03"
|
||||
+ _ = session.Rollback()
|
||||
+ _ = session.Close()
|
||||
+ require.True(t, blocked, "an uncooperative writer escaped the actual table lock")
|
||||
+ }
|
||||
+ releaseLock()
|
||||
+ select {
|
||||
+ case err := <-done:
|
||||
+ require.NoError(t, err)
|
||||
+ case <-time.After(5 * time.Second):
|
||||
+ t.Fatal("transaction did not finish")
|
||||
+ }
|
||||
+ for _, statement := range statements {
|
||||
+ _, err := raw.Exec(statement)
|
||||
+ require.NoError(t, err)
|
||||
+ }
|
||||
+ })
|
||||
+ run("concurrent-preparation-has-one-owner", func(t *testing.T) {
|
||||
+ start, done := make(chan struct{}), make(chan error, 2)
|
||||
+ for _, id := range []string{"postgres-concurrent-first", "postgres-concurrent-second"} {
|
||||
+ go func(id string) {
|
||||
+ <-start
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, id, "postgres-concurrent-case", 0, operationSource())
|
||||
+ done <- err
|
||||
+ }(id)
|
||||
+ }
|
||||
+ close(start)
|
||||
+ ok, conflict := 0, 0
|
||||
+ for range 2 {
|
||||
+ select {
|
||||
+ case err := <-done:
|
||||
+ if err == nil {
|
||||
+ ok++
|
||||
+ } else if errors.Is(err, auth.ErrSourceOperationConflict) {
|
||||
+ conflict++
|
||||
+ } else {
|
||||
+ t.Fatal("unexpected concurrent preparation error")
|
||||
+ }
|
||||
+ case <-time.After(5 * time.Second):
|
||||
+ t.Fatal("concurrent preparation timed out")
|
||||
+ }
|
||||
+ }
|
||||
+ require.Equal(t, 1, ok)
|
||||
+ require.Equal(t, 1, conflict)
|
||||
+ })
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func TestSourceOperationPostgresProcessRecovery(t *testing.T) {
|
||||
+ step := os.Getenv("CUSTODIAN_PG_STEP")
|
||||
+ require.NotEmpty(t, step, "explicit synthetic recovery phase required")
|
||||
+ ctx, engine, _ := postgresOperationDB(t, "custodian_recovery")
|
||||
+ switch step {
|
||||
+ case "prepare":
|
||||
+ resetPostgresOperations(t, engine)
|
||||
+ _, err := auth.PrepareSourceOperation(ctx, "postgres-process-operation", "postgres-process-case", 0, operationSource())
|
||||
+ require.NoError(t, err)
|
||||
+ case "crash":
|
||||
+ engine.AddHook(operationCrashAfterSourceInsert{})
|
||||
+ _, _ = auth.ApplySourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ t.Fatal("actual source insert did not reach the process-death hook")
|
||||
+ case "recover-prepared":
|
||||
+ r, err := auth.ReconcileSourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "prepared", r.Phase)
|
||||
+ n, err := engine.Table("login_source").Count()
|
||||
+ require.NoError(t, err)
|
||||
+ require.Zero(t, n)
|
||||
+ _, err = auth.ApplySourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ require.NoError(t, err)
|
||||
+ case "recover-applied":
|
||||
+ r, err := auth.ReconcileSourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "applied", r.Phase)
|
||||
+ n, err := engine.Table("login_source").Count()
|
||||
+ require.NoError(t, err)
|
||||
+ require.EqualValues(t, 1, n)
|
||||
+ _, err = auth.InverseSourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ require.NoError(t, err)
|
||||
+ case "recover-inverse":
|
||||
+ r, err := auth.ReconcileSourceOperation(ctx, "postgres-process-operation", "postgres-process-case")
|
||||
+ require.NoError(t, err)
|
||||
+ require.Equal(t, "reverted", r.Phase)
|
||||
+ n, err := engine.Table("login_source").Count()
|
||||
+ require.NoError(t, err)
|
||||
+ require.Zero(t, n)
|
||||
+ default:
|
||||
+ t.Fatal("unknown synthetic recovery phase")
|
||||
+ }
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/models/auth/source_operation_process_test.go
|
||||
@@ -0,0 +1,97 @@
|
||||
+// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
|
|
@ -1276,12 +1658,15 @@
|
|||
return x.Context(ctx)
|
||||
--- a/models/db/engine.go
|
||||
+++ b/models/db/engine.go
|
||||
@@ -330,8 +330,12 @@
|
||||
@@ -330,8 +330,15 @@
|
||||
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
ctx, task := trace.NewTask(c.Ctx, "sql")
|
||||
ctx = context.WithValue(ctx, sqlTask{}, task)
|
||||
- trace.Log(ctx, "query", c.SQL)
|
||||
- trace.Logf(ctx, "args", "%v", c.Args)
|
||||
+ // XORM passes the same ContextHook to every BeforeProcess hook. Publish
|
||||
+ // the task there too, so a later hook returning c.Ctx retains it.
|
||||
+ c.Ctx = ctx
|
||||
+ if isSensitiveSQL(c.Ctx) {
|
||||
+ trace.Log(ctx, "query", "[owner-confined query]")
|
||||
+ } else {
|
||||
|
|
@ -1291,7 +1676,7 @@
|
|||
return ctx, nil
|
||||
}
|
||||
|
||||
@@ -353,7 +357,11 @@
|
||||
@@ -353,7 +360,11 @@
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Treshold {
|
||||
|
|
@ -1304,7 +1689,7 @@
|
|||
}
|
||||
return nil
|
||||
}
|
||||
@@ -370,7 +378,11 @@
|
||||
@@ -370,7 +381,11 @@
|
||||
|
||||
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
|
||||
|
|
@ -1520,6 +1905,63 @@
|
|||
+ }
|
||||
+ }
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/models/db/tracing_context_test.go
|
||||
@@ -0,0 +1,54 @@
|
||||
+package db
|
||||
+
|
||||
+import (
|
||||
+ "bytes"
|
||||
+ "context"
|
||||
+ "runtime/trace"
|
||||
+ "testing"
|
||||
+
|
||||
+ "xorm.io/xorm"
|
||||
+ "xorm.io/xorm/contexts"
|
||||
+)
|
||||
+
|
||||
+type followingQueryHook struct{}
|
||||
+
|
||||
+func (followingQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
+ return c.Ctx, nil
|
||||
+}
|
||||
+func (followingQueryHook) AfterProcess(*contexts.ContextHook) error { return nil }
|
||||
+
|
||||
+func TestTracingContextSurvivesFollowingHooks(t *testing.T) {
|
||||
+ for _, sensitive := range []bool{false, true} {
|
||||
+ for _, fail := range []bool{false, true} {
|
||||
+ engine, err := xorm.NewEngine("sqlite3", ":memory:")
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ engine.AddHook(TracingHook{})
|
||||
+ engine.AddHook(followingQueryHook{})
|
||||
+ ctx := context.Background()
|
||||
+ if sensitive {
|
||||
+ ctx = WithSensitiveSQL(ctx)
|
||||
+ }
|
||||
+ var output bytes.Buffer
|
||||
+ if err := trace.Start(&output); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ func() {
|
||||
+ defer trace.Stop()
|
||||
+ query := "SELECT ?"
|
||||
+ if fail {
|
||||
+ query = "SELECT missing_synthetic_function(?)"
|
||||
+ }
|
||||
+ _, err = engine.Context(ctx).Exec(query, "synthetic-following-hook-sentinel")
|
||||
+ }()
|
||||
+ _ = engine.Close()
|
||||
+ if (err != nil) != fail {
|
||||
+ t.Fatal("unexpected real SQLite query outcome")
|
||||
+ }
|
||||
+ if bytes.Contains(output.Bytes(), []byte("synthetic-following-hook-sentinel")) == sensitive {
|
||||
+ t.Fatal("following hook changed private or ordinary trace disclosure")
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
--- a/routers/web/admin/auths.go
|
||||
+++ b/routers/web/admin/auths.go
|
||||
@@ -322,6 +322,13 @@
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
"forgejo_version": "11.0.3",
|
||||
"source_archive_sha256": "1ddd8c926ca07d40436897971458ace7dcf1d3496a0813ab9e0d869edef2365c",
|
||||
"source_commit": "b81aa4d0807cc1e2d6a2fbafd0b75c46cc0ae237",
|
||||
"patch_sha256": "15cb7f13b1b1d1095cc344176ca337f705333a436a70ae803fcfd3d7f802ccf4",
|
||||
"patch_sha256": "feb1079399a43989742ae0a946c1ada50113d1821e6541f97f0b5592ee1fc20c",
|
||||
"go_archive_sha256": "3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8",
|
||||
"go_version": "1.24.3",
|
||||
"node_archive_sha256": "dafe2e8f82cb97de1bd10db9e2ec4c07bbf53389b0799b1e095a918951e78fd4",
|
||||
"node_version": "22.15.0",
|
||||
"source_date_epoch": 1752105237,
|
||||
"release_version": "11.0.3+cust80.15cb7f13",
|
||||
"release_version": "11.0.3+cust80.feb10793",
|
||||
"tags": "bindata timetzdata sqlite sqlite_unlock_notify",
|
||||
"runtime_base": "code.forgejo.org/forgejo/forgejo@sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b",
|
||||
"runtime_index_digest": "sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba",
|
||||
|
|
@ -43,6 +43,14 @@
|
|||
"path": "models/auth/source_operation_internal_test.go",
|
||||
"sha256": "af0f424d3a1fd2b29793b64baa1fa07284fee7ea0b7721c503db4efac0d871cb"
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_postgres_export_test.go",
|
||||
"sha256": "b9d1553144766b15c408295b87506106935abf4ed61460598b58dc81825e808d"
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_postgres_test.go",
|
||||
"sha256": "4a37504e5af00580d114ef1bed5cb32e09e4ec2de5fd3192adef043a4ea5f7fe"
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_process_test.go",
|
||||
"sha256": "40d33b11a01cb659f0bf6f8b545c4d79910e8c082a789060bcbc9cadeee965bd"
|
||||
|
|
@ -57,7 +65,7 @@
|
|||
},
|
||||
{
|
||||
"path": "models/db/engine.go",
|
||||
"sha256": "43683804d0c5bc6ead1dba0344c69dd196825cf3d5eb502af8d932a7b8207b96"
|
||||
"sha256": "d21a5b0b9ca37ac48222b79007467a232abc38edda99b3c5f54f1f4c5ab795e8"
|
||||
},
|
||||
{
|
||||
"path": "models/db/sensitive_sql.go",
|
||||
|
|
@ -71,6 +79,10 @@
|
|||
"path": "models/db/sensitive_sql_test.go",
|
||||
"sha256": "de6353c955047c3311747e4777b9f9b2195e9efb19160b971dd758dfc94cf31b"
|
||||
},
|
||||
{
|
||||
"path": "models/db/tracing_context_test.go",
|
||||
"sha256": "612b60c67a2be7e2da9173b95d5ef5abd7b9e1e377360828e39a622610bba070"
|
||||
},
|
||||
{
|
||||
"path": "routers/web/admin/auths.go",
|
||||
"sha256": "3498229e06fb9ff52396bcb5e68c0cc2d2ad24e6494533d8717d7c833f3e2923"
|
||||
|
|
@ -182,5 +194,13 @@
|
|||
"runtime_base_config_sha256": "bc5a3df111729582faadebd01d126ba769e2b65d0c62f306f6bc79f4e562452b",
|
||||
"patch_file": "forgejo-security-extension.patch",
|
||||
"candidate_review_status": "coordinator tested; independent owner review pending",
|
||||
"producer_source_commit": "ca0cb75729a63c9288b7333382d0fa3eb1e8c9a3"
|
||||
"producer_source_commit": "ca0cb75729a63c9288b7333382d0fa3eb1e8c9a3",
|
||||
"optional_source_tests": {
|
||||
"source_operation_postgres": {
|
||||
"database_major": 16,
|
||||
"synthetic_fixture_image": "postgres@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777",
|
||||
"source_only": true,
|
||||
"production_connection_or_credentials": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,6 +242,33 @@ If links prevent removal, retain the extended image and reconcile. The earlier
|
|||
T02 single-Helm-rollback statement is not sufficient recovery for this extension;
|
||||
its historical text is retained and corrected here.
|
||||
|
||||
2026-10-18 PostgreSQL/source-context continuation for T08: actual Forgejo model
|
||||
schemas, OAuth configuration types and production DB initialization run on a
|
||||
disposable PostgreSQL 16.14 instance. All 37 test/subtest results pass across
|
||||
default, explicit-public and named schemas. Separate client processes prove
|
||||
rollback after process death immediately following INSERT, committed apply
|
||||
survival through a real server restart, and subsequent reconciliation/inverse.
|
||||
Exact raw restoration, private drift, linked-user/external-link refusal, trigger
|
||||
rollback, table-writer exclusion and concurrent preparation are covered.
|
||||
Controlled copies with the table lock or private-query context removed fail the
|
||||
corresponding tests in all three schema profiles. All owned fixture containers
|
||||
and volumes were removed; no deployed database or real credential was used.
|
||||
|
||||
The diagnostics check exposed a real tracing-hook composition panic. The new
|
||||
40-path build candidate publishes the task context into XORM's shared query
|
||||
context so subsequent hooks preserve it. Actual private/ordinary SQLite traces
|
||||
and PostgreSQL driver-error hooks cover the correction. Six focused SQLite DB
|
||||
checks and all 94 selected composed package results pass without skips. The
|
||||
previous panic, fixture/compilation failures and refinement history remain in
|
||||
Custodian forgejo-postgres-source-r1. Complete release evidence uses the new
|
||||
feb10793 patch; the prior frozen artifact is historical.
|
||||
|
||||
This proves bounded source/database mechanics. T08 remains progress for current
|
||||
authenticated initiating/inverse sessions, release-wide writer exclusion, private
|
||||
evidence/custody/retention and native deployed-database acceptance. Independent
|
||||
owner review and exact package/type/runtime admission remain open. T07/T09 and
|
||||
all original task UUIDs and statuses are preserved.
|
||||
|
||||
## T09 — Enforce authenticated Access Engine decisions in the application path
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue