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
120 lines
3.4 KiB
Go
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
|
|
}
|