Files
sm-user-plugin/classes/privileged.go
Jakub Zych f4969591e2 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
2026-10-05 14:53:41 +02:00

120 lines
3.4 KiB
Go

package classes
import (
"context"
"fmt"
"strings"
"git.golem15.com/golem15/summercms/modules/compass"
"gorm.io/gorm"
)
// PermissionManagePrivilegedGroups is the backend permission an administrator
// needs, on top of golem15.users.access_users, to change the email or the
// password of a member of a privileged group, or to delete such a user for
// good.
const PermissionManagePrivilegedGroups = "golem15.users.manage_privileged_groups"
// privilegedGroupsKey is the config list of privileged group codes.
const privilegedGroupsKey = "golem15.user.privileged_groups"
// defaultPrivilegedGroup is the only privileged code when the config names
// none: the site-admin group.
const defaultPrivilegedGroup = "admin"
// PrivilegedGroupCodes returns the group codes whose members are protected,
// read from golem15.user.privileged_groups on every call so a config change
// applies without a restart. A nil config or a missing key gives the single
// code admin. Blank entries are dropped; a key that is present and empty
// means that no group is privileged.
func PrivilegedGroupCodes(cfg *compass.Config) []string {
if cfg == nil {
return []string{defaultPrivilegedGroup}
}
raw, ok := cfg.Lookup(privilegedGroupsKey)
if !ok {
return []string{defaultPrivilegedGroup}
}
var entries []string
switch v := raw.(type) {
case []string:
entries = v
case []any:
for _, item := range v {
if s, ok := item.(string); ok {
entries = append(entries, s)
}
}
case string:
// An environment override arrives as one comma-separated string.
entries = strings.Split(v, ",")
}
codes := make([]string, 0, len(entries))
for _, entry := range entries {
if code := strings.TrimSpace(entry); code != "" {
codes = append(codes, code)
}
}
return codes
}
// IsPrivilegedCode reports whether a group's code is in codes. Codes compare
// case-sensitively, as HasGroupCode does, and a group without a code is
// never privileged.
func IsPrivilegedCode(codes []string, code *string) bool {
if code == nil || *code == "" {
return false
}
for _, privileged := range codes {
if privileged == *code {
return true
}
}
return false
}
// PrivilegedGroupIDs returns the ids of the user_groups rows whose code is in
// codes, ordered by id.
func PrivilegedGroupIDs(ctx context.Context, db *gorm.DB, codes []string) ([]uint, error) {
if db == nil {
return nil, fmt.Errorf("golem15.user: gorm handle is missing")
}
ids := []uint{}
if len(codes) == 0 {
return ids, nil
}
err := fresh(ctx, db).
Table("user_groups").
Where("code IN ?", codes).
Order("id").
Pluck("id", &ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
// 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. 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")
}
if len(codes) == 0 || userID == 0 {
return false, nil
}
held, err := UserGroupCodes(ctx, fresh(ctx, db), userID)
if err != nil {
return false, err
}
for i := range held {
if IsPrivilegedCode(codes, &held[i]) {
return true, nil
}
}
return false, nil
}