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