diff --git a/build/forgejo/README.md b/build/forgejo/README.md index 56d2a7b..9a0562e 100644 --- a/build/forgejo/README.md +++ b/build/forgejo/README.md @@ -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. diff --git a/build/forgejo/forgejo-security-extension.patch b/build/forgejo/forgejo-security-extension.patch index cd0fb52..9465e3f 100644 --- a/build/forgejo/forgejo-security-extension.patch +++ b/build/forgejo/forgejo-security-extension.patch @@ -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 @@ diff --git a/build/forgejo/inputs.lock.json b/build/forgejo/inputs.lock.json index 645c5cb..d3259d0 100644 --- a/build/forgejo/inputs.lock.json +++ b/build/forgejo/inputs.lock.json @@ -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 + } + } } diff --git a/workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md b/workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md index e733dd0..d53106e 100644 --- a/workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md +++ b/workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md @@ -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