Files
sm-user-plugin/models/permission_set.go
Jakub Zych a3992879db feat: User Groups admin screen with the users count and privileged codes protected
- controller golem15.user.usergroups behind golem15.users.access_groups
- list with users_count from the list's own query; form with the checkbox
  permission editor stored in user_groups.permissions
- creating a group with a privileged code, changing a code to or from one and
  deleting a privileged group need golem15.users.manage_privileged_groups
- a group delete removes its users_groups rows in the same transaction
- UserGroup gains Fillable, Rules and the read-only UsersCount; PermissionSet.Text
- TestAdminGroups and the D-06 matrix in TestAdminPrivilegedGroups
2026-10-05 13:52:26 +02:00

125 lines
3.4 KiB
Go

package models
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
)
// PermissionSet is a frontend permission map as Winter stores it in
// users.permissions and user_groups.permissions: a JSON object of permission
// code to integer, where 1 allows, -1 denies and 0 (or no entry) inherits.
//
// Reading is tolerant, because the rows a cutover imports were written by
// several PHP code paths: NULL, an empty string, the texts [] and null all
// mean "no permissions", and a value may be an integer or a numeric string.
// Writing is strict: an object of integers without zero values, or NULL for
// an empty set.
type PermissionSet map[string]int
// GormDataType tells GORM the column type: a map is not a type it knows.
func (PermissionSet) GormDataType() string { return "text" }
// Scan implements sql.Scanner. An entry whose value is not numeric is
// skipped; content that is not a JSON object (and none of the empty forms
// above) is an error, so a damaged row is never read as "no permissions",
// which would silently drop its denials.
func (p *PermissionSet) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case nil:
*p = PermissionSet{}
return nil
case string:
raw = []byte(v)
case []byte:
raw = v
default:
return fmt.Errorf("golem15.user: cannot scan %T into a permission set", src)
}
set, err := ParsePermissionSet(raw)
if err != nil {
return err
}
*p = set
return nil
}
// ParsePermissionSet decodes the stored text of a permission column with the
// tolerance Scan documents.
func ParsePermissionSet(raw []byte) (PermissionSet, error) {
text := strings.TrimSpace(string(raw))
if text == "" || text == "[]" || text == "null" {
return PermissionSet{}, nil
}
dec := json.NewDecoder(bytes.NewReader([]byte(text)))
dec.UseNumber()
var entries map[string]any
if err := dec.Decode(&entries); err != nil {
return nil, fmt.Errorf("golem15.user: stored permissions are not a JSON object: %w", err)
}
if dec.More() {
return nil, fmt.Errorf("golem15.user: stored permissions carry trailing data")
}
set := make(PermissionSet, len(entries))
for code, value := range entries {
if n, ok := permissionInt(value); ok {
set[code] = n
}
}
return set, nil
}
// permissionInt reads one stored permission value: a JSON integer or a
// string holding one. Anything else is not numeric.
func permissionInt(value any) (int, bool) {
var text string
switch v := value.(type) {
case json.Number:
text = v.String()
case string:
text = strings.TrimSpace(v)
default:
return 0, false
}
n, err := strconv.Atoi(text)
if err != nil {
return 0, false
}
return n, true
}
// Text returns the set as the text of a permission column that is a plain
// nullable string on its model (user_groups.permissions): the JSON object
// Value writes, or nil for a set without a non-zero value.
func (p PermissionSet) Text() *string {
value, err := p.Value()
text, ok := value.(string)
if err != nil || !ok {
return nil
}
return &text
}
// Value implements driver.Valuer: NULL for a set without a non-zero value,
// else a JSON object of integers with the zero values left out.
func (p PermissionSet) Value() (driver.Value, error) {
kept := make(map[string]int, len(p))
for code, value := range p {
if value != 0 {
kept[code] = value
}
}
if len(kept) == 0 {
return nil, nil
}
raw, err := json.Marshal(kept)
if err != nil {
return nil, err
}
return string(raw), nil
}