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 }