CodeSampleX

示例

gorm.io/gorm v1.31.2: Keep a tenant scope from being bypassed by an ungrouped Or while inspecting GORM DryRun SQL

已验证示例 — golang gorm.io/gorm v1.31.2: Keep a tenant scope from being bypassed by an ungrouped Or while inspecting GORM DryRun SQL. contract 在 go 1.26 · linux…

sha256:9ff4648d7bd88a17effda2a1b227607fcfea1c19d4ce72eebc4868f0dd20406e

本网络只提供一件事:能构建的样本。它在沙箱中运行并保留签名回执。它不评级、不担保——同样的代码能否在你的环境构建,它没有测量过。 提交了通过的契约回执的不同签名密钥数量。为 1 表示只有作者;大于 1 表示还有其他人构建过。密钥是自行生成的,背后没有注册身份,因此计的是密钥而非人。 MIT-0

执行证据

声明的环境与签名的运行分开呈现,你可以看到这个样本究竟运行了什么、在哪里运行。

证据依据
签名契约通过
验证回执
2
构建过它的签名密钥
2
声明的环境 go 1.26 linux x64 go 1.26 go go

验证运行环境

环境 契约 阶段 运行日期
go 1.26 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-17
go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · golang@1
2026-08-18

案例

HOW
目标
Keep a tenant scope from being bypassed by an ungrouped Or while inspecting GORM DryRun SQL
包
符号
  • gorm.Open
  • sqlite.New
  • gorm.Config.DryRun
  • gorm.Config.DisableAutomaticPing
  • DB.Scopes
  • DB.Where
  • DB.Or
  • DB.Find
环境
go 1.26
创建时间
2026-08-17T01:32:02Z

契约

  1. assert Scopes(Tenant(7)).Where("active = ?", true).Or("role = ?", "admin") generates WHERE active = ? OR role = ? AND tenant_id = ?, so SQL precedence allows active rows from other tenants
  2. assert the ungrouped statement binds [true, "admin", uint(7)] in that order, proving the lazily executed tenant scope is appended after the chained conditions
  3. assert passing Where("active = ?", true).Or("role = ?", "admin") as one nested Where condition generates WHERE (active = ? OR role = ?) AND tenant_id = ? with the same bind values
  4. assert DryRun Find executes no SQL against database/sql; only the official SQLite dialector's required select sqlite_version() initialization probe reaches the in-process stub

文件

  • NOTES.md
  • csx.json
  • go.mod
  • go.sum
  • scope.go
  • scope_test.go

下载源代码构件 (tar.gz)

源代码

NOTES.md
The required known-solution lookup returned GORM association and SQLite soft-delete samples, but none covered lazy scopes combined with `Or`.

This fails silently: the query builds successfully, yet normal SQL precedence lets the ungrouped branch match rows outside the tenant. Build the alternatives as a nested `Where` condition before applying the scope.
csx.json
{"case":{"believed":"Calling Scopes(Tenant(id)) before Where(...).Or(...) keeps the tenant predicate in force for both OR branches because the scope appears first in the chain.","caseId":"case:sha256:531888b3c2ec7554cbbcbbe167662c6ec753768b313eba8aec8b282f5bafc166","contract":["assert Scopes(Tenant(7)).Where(\"active = ?\", true).Or(\"role = ?\", \"admin\") generates WHERE active = ? OR role = ? AND tenant_id = ?, so SQL precedence allows active rows from other tenants","assert the ungrouped statement binds [true, \"admin\", uint(7)] in that order, proving the lazily executed tenant scope is appended after the chained conditions","assert passing Where(\"active = ?\", true).Or(\"role = ?\", \"admin\") as one nested Where condition generates WHERE (active = ? OR role = ?) AND tenant_id = ? with the same bind values","assert DryRun Find executes no SQL against database/sql; only the official SQLite dialector's required select sqlite_version() initialization probe reaches the in-process stub"],"goal":"Keep a tenant scope from being bypassed by an ungrouped Or while inspecting GORM DryRun SQL","kind":"HOW","packages":["pkg:golang/gorm.io/gorm@v1.31.2","pkg:golang/gorm.io/driver/sqlite@v1.6.0"],"schemaVersion":1,"symbols":["gorm.Open","sqlite.New","gorm.Config.DryRun","gorm.Config.DisableAutomaticPing","DB.Scopes","DB.Where","DB.Or","DB.Find"]},"contractCommand":["go","test","-mod=readonly","-count=1","./..."],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"go","runtime":"go","runtimeVersion":"1.26","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/gorm.io/gorm@v1.31.2","pkg:golang/gorm.io/driver/sqlite@v1.6.0"],"schemaVersion":1,"symbols":["gorm.Open","sqlite.New","gorm.Config.DryRun","gorm.Config.DisableAutomaticPing","DB.Scopes","DB.Where","DB.Or","DB.Find"],"verifierAdapter":"golang@1"}
go.mod
module example.com/gorm-scope-or

go 1.22.0

require (
	gorm.io/driver/sqlite v1.6.0
	gorm.io/gorm v1.31.2
)

require (
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/jinzhu/now v1.1.5 // indirect
	github.com/mattn/go-sqlite3 v1.14.22 // indirect
	golang.org/x/text v0.20.0 // indirect
)
go.sum
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
scope.go
package gormscopeor

import "gorm.io/gorm"

// User is deliberately small: only the columns involved in the visibility
// predicate matter to the generated SQL contract.
type User struct {
	ID       uint
	TenantID uint
	Active   bool
	Role     string
}

// Tenant is a common multi-tenant scope. Scopes are evaluated lazily when a
// finisher such as Find runs, even if Scopes appears first in the call chain.
func Tenant(id uint) func(*gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("tenant_id = ?", id)
	}
}

// UngroupedVisibility looks plausible, but SQL AND binds more tightly than
// OR. The tenant scope therefore protects only one side of the OR expression.
func UngroupedVisibility(db *gorm.DB, tenantID uint) *gorm.DB {
	var users []User
	return db.Scopes(Tenant(tenantID)).
		Where("active = ?", true).
		Or("role = ?", "admin").
		Find(&users)
}

// GroupedVisibility makes the alternatives one nested condition before the
// tenant scope is added. The tenant predicate then applies to both branches.
func GroupedVisibility(db *gorm.DB, tenantID uint) *gorm.DB {
	var users []User
	alternatives := db.Where("active = ?", true).Or("role = ?", "admin")
	return db.Scopes(Tenant(tenantID)).Where(alternatives).Find(&users)
}
scope_test.go
package gormscopeor

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"reflect"
	"sync"
	"sync/atomic"
	"testing"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var probeSequence atomic.Uint64

type probeState struct {
	mu      sync.Mutex
	queries []string
}

func (s *probeState) record(query string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.queries = append(s.queries, query)
}

func (s *probeState) snapshot() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return append([]string(nil), s.queries...)
}

type probeDriver struct{ state *probeState }

func (d probeDriver) Open(string) (driver.Conn, error) {
	return probeConn{state: d.state}, nil
}

type probeConn struct{ state *probeState }

func (c probeConn) Prepare(string) (driver.Stmt, error) {
	return nil, fmt.Errorf("unexpected Prepare call")
}

func (probeConn) Close() error { return nil }

func (probeConn) Begin() (driver.Tx, error) {
	return nil, fmt.Errorf("unexpected Begin call")
}

func (c probeConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
	c.state.record(query)
	if query != "select sqlite_version()" {
		return nil, fmt.Errorf("unexpected executed query %q", query)
	}
	return &versionRows{}, nil
}

type versionRows struct{ sent bool }

func (*versionRows) Columns() []string { return []string{"sqlite_version()"} }
func (*versionRows) Close() error      { return nil }

func (r *versionRows) Next(values []driver.Value) error {
	if r.sent {
		return io.EOF
	}
	r.sent = true
	values[0] = "3.45.0"
	return nil
}

func openDryRun(t *testing.T) (*gorm.DB, *probeState) {
	t.Helper()

	state := &probeState{}
	driverName := fmt.Sprintf("gorm_scope_probe_%d", probeSequence.Add(1))
	sql.Register(driverName, probeDriver{state: state})
	conn, err := sql.Open(driverName, "")
	if err != nil {
		t.Fatalf("open in-process probe: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	db, err := gorm.Open(sqlite.New(sqlite.Config{Conn: conn}), &gorm.Config{
		DryRun:               true,
		DisableAutomaticPing: true,
	})
	if err != nil {
		t.Fatalf("open GORM dry-run handle: %v", err)
	}
	return db, state
}

func TestTenantScopeRequiresExplicitOrGrouping(t *testing.T) {
	db, probe := openDryRun(t)

	ungrouped := UngroupedVisibility(db, 7)
	if ungrouped.Error != nil {
		t.Fatalf("build ungrouped statement: %v", ungrouped.Error)
	}
	const wantUngrouped = "SELECT * FROM `users` WHERE active = ? OR role = ? AND tenant_id = ?"
	if got := ungrouped.Statement.SQL.String(); got != wantUngrouped {
		t.Fatalf("ungrouped SQL\n got: %s\nwant: %s", got, wantUngrouped)
	}
	if want := []any{true, "admin", uint(7)}; !reflect.DeepEqual(ungrouped.Statement.Vars, want) {
		t.Fatalf("ungrouped vars = %#v, want %#v", ungrouped.Statement.Vars, want)
	}

	grouped := GroupedVisibility(db, 7)
	if grouped.Error != nil {
		t.Fatalf("build grouped statement: %v", grouped.Error)
	}
	const wantGrouped = "SELECT * FROM `users` WHERE (active = ? OR role = ?) AND tenant_id = ?"
	if got := grouped.Statement.SQL.String(); got != wantGrouped {
		t.Fatalf("grouped SQL\n got: %s\nwant: %s", got, wantGrouped)
	}
	if want := []any{true, "admin", uint(7)}; !reflect.DeepEqual(grouped.Statement.Vars, want) {
		t.Fatalf("grouped vars = %#v, want %#v", grouped.Statement.Vars, want)
	}

	// The official SQLite dialector performs this one initialization probe.
	// Neither Find statement may reach database/sql while DryRun is enabled.
	if got, want := probe.snapshot(), []string{"select sqlite_version()"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("executed queries = %#v, want only dialector initialization %#v", got, want)
	}
}

原始种子者

csx-seed