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
This commit is contained in:
48
classes/admin_actions.go
Normal file
48
classes/admin_actions.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package classes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-user-plugin/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ThrottleStates reads the ban and suspension state of userIDs from
|
||||
// user_throttle in one query. A user is banned when any of their rows is
|
||||
// banned, and suspended when a row is suspended and its suspended_at lies
|
||||
// inside the suspension window (golem15.user.throttle.suspension_minutes,
|
||||
// read from ctx as ContextWithThrottle stores it). It is a pure read: an
|
||||
// expired suspension is not lifted here.
|
||||
func ThrottleStates(ctx context.Context, db *gorm.DB, userIDs []uint) (banned, suspended map[uint]bool, err error) {
|
||||
banned, suspended = map[uint]bool{}, map[uint]bool{}
|
||||
if db == nil {
|
||||
return nil, nil, fmt.Errorf("golem15.user: gorm handle is missing")
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
return banned, suspended, nil
|
||||
}
|
||||
minutes := throttleFrom(ctx).minutes
|
||||
if minutes <= 0 {
|
||||
minutes = 15
|
||||
}
|
||||
window := time.Duration(minutes) * time.Minute
|
||||
var rows []models.Throttle
|
||||
err = db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).
|
||||
Where("user_id IN ? AND (is_banned OR is_suspended)", userIDs).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
for _, row := range rows {
|
||||
if row.IsBanned {
|
||||
banned[row.UserID] = true
|
||||
}
|
||||
if row.IsSuspended && row.SuspendedAt != nil && now.Before(row.SuspendedAt.Add(window)) {
|
||||
suspended[row.UserID] = true
|
||||
}
|
||||
}
|
||||
return banned, suspended, nil
|
||||
}
|
||||
118
classes/privileged.go
Normal file
118
classes/privileged.go
Normal file
@@ -0,0 +1,118 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user