示例
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
契约
- 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
文件
- NOTES.md
- csx.json
- go.mod
- go.sum
- scope.go
- scope_test.go
源代码
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.
{"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"}
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
)
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=
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)
}
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)
}
}