Files
sm-user-plugin/classes/privileged.go
Jakub Zych b410a7fea9 feat: register the Users admin screen with the privileged-member guard
- permissions (the four Winter codes and manage_privileged_groups),
  the user navigation item and the embedded admin YAML
- Users controller: list with deactivated users, row states, the
  groups, created date and activated filters, and the form's name,
  surname and email
- a member of a group in golem15.user.privileged_groups (default
  admin) keeps their email, password and row unless the administrator
  holds golem15.users.manage_privileged_groups; the refusal is a 403
  that saves nothing
2026-10-05 13:01:48 +02:00

119 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 := db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).
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.
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, db.Session(&gorm.Session{NewDB: true}), userID)
if err != nil {
return false, err
}
for i := range held {
if IsPrivilegedCode(codes, &held[i]) {
return true, nil
}
}
return false, nil
}