From f4969591e22b5f7f842ecb3b4b503afb72cf3152 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 5 Oct 2026 14:53:41 +0200 Subject: [PATCH] fix: read privileged membership and the admin actions through a clean statement (T-12.1-38, T-12.1-39) classes.fresh chained WithContext onto a lazy NewDB session, which clones the caller's statement with its conditions. A handle that carried a condition made IsPrivilegedMember answer false for a member of a privileged group, so the takeover guard would have let the request through. No caller passes such a handle today: the admin hooks hand over the write's transaction handle. - fresh builds the empty statement before anything is chained onto it - IsPrivilegedMember and PrivilegedGroupIDs read through fresh --- classes/admin_actions.go | 8 ++++++-- classes/privileged.go | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/classes/admin_actions.go b/classes/admin_actions.go index 946f125..4565d06 100644 --- a/classes/admin_actions.go +++ b/classes/admin_actions.go @@ -248,9 +248,13 @@ func ForceDeleteCleanup(ctx context.Context, db *gorm.DB, bucket *blob.Bucket, u } // fresh returns db with an empty statement on ctx, so a query never continues -// from conditions the caller's handle already carries. +// from conditions the caller's handle already carries. A NewDB session is +// lazy: GORM builds the empty statement on the next chain method, and a +// WithContext chained directly onto it clones the caller's statement with its +// conditions instead. Clauses() makes the empty statement exist first, so the +// handle stays clean whatever is chained next. func fresh(ctx context.Context, db *gorm.DB) *gorm.DB { - return db.Session(&gorm.Session{NewDB: true}).WithContext(ctx) + return db.Session(&gorm.Session{NewDB: true, Context: ctx}).Clauses().Session(&gorm.Session{NewDB: true}) } // userIDs returns the ids of users, skipping nil and unsaved entries. diff --git a/classes/privileged.go b/classes/privileged.go index b9633ad..0bcc80f 100644 --- a/classes/privileged.go +++ b/classes/privileged.go @@ -83,7 +83,7 @@ func PrivilegedGroupIDs(ctx context.Context, db *gorm.DB, codes []string) ([]uin if len(codes) == 0 { return ids, nil } - err := db.Session(&gorm.Session{NewDB: true}).WithContext(ctx). + err := fresh(ctx, db). Table("user_groups"). Where("code IN ?", codes). Order("id"). @@ -97,7 +97,8 @@ func PrivilegedGroupIDs(ctx context.Context, db *gorm.DB, codes []string) ([]uin // IsPrivilegedMember reports whether the user belongs to at least one group // whose code is in codes. A user in no group, and a group without a code, // give false. A deactivated user's membership counts: it survives the soft -// delete. +// delete. The membership is read through an empty statement on db's +// connection, so conditions the caller's handle carries never narrow it. func IsPrivilegedMember(ctx context.Context, db *gorm.DB, codes []string, userID uint) (bool, error) { if db == nil { return false, fmt.Errorf("golem15.user: gorm handle is missing") @@ -105,7 +106,7 @@ func IsPrivilegedMember(ctx context.Context, db *gorm.DB, codes []string, userID if len(codes) == 0 || userID == 0 { return false, nil } - held, err := UserGroupCodes(ctx, db.Session(&gorm.Session{NewDB: true}), userID) + held, err := UserGroupCodes(ctx, fresh(ctx, db), userID) if err != nil { return false, err }