- PermissionSet reads the stored JSON tolerantly and writes an object of integers; FrontendPermission is the catalogue row - MergedPermissions, HasPermission and UserHasPermission port PHP getMergedPermissions and Winter's hasPermission: groups merge with most positive wins, the user's own values override, held means exactly 1, wildcards as in Winter - User gains Permissions (never serialized) and declares the avatar attachment relation the user API already writes
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// MergedPermissions returns the frontend permissions of a user as PHP
|
|
// User::getMergedPermissions builds them:
|
|
//
|
|
// 1. the permissions of every group the user belongs to are merged, and
|
|
// where two groups name the same code the higher value wins ("most
|
|
// positive wins": 1 over 0 over -1);
|
|
// 2. the user's own permissions are laid over the result, and a user value
|
|
// replaces the groups' value whatever it is (a user-level -1 overrides a
|
|
// group's 1, and a user-level 1 overrides a group's -1).
|
|
//
|
|
// The result maps a code to 1 (allowed), -1 (denied) or 0. Use HasPermission
|
|
// to ask it a question. A deactivated user's permissions are read as well.
|
|
func MergedPermissions(ctx context.Context, db *gorm.DB, userID uint) (map[string]int, error) {
|
|
if db == nil {
|
|
return nil, fmt.Errorf("golem15.user: gorm handle is missing")
|
|
}
|
|
var groups []struct{ Permissions models.PermissionSet }
|
|
err := fresh(ctx, db).
|
|
Table("user_groups").
|
|
Select("user_groups.permissions").
|
|
Joins("JOIN users_groups ON users_groups.user_group_id = user_groups.id").
|
|
Where("users_groups.user_id = ?", userID).
|
|
Order("user_groups.id").
|
|
Scan(&groups).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged := map[string]int{}
|
|
for _, group := range groups {
|
|
for code, value := range group.Permissions {
|
|
if held, ok := merged[code]; !ok || value > held {
|
|
merged[code] = value
|
|
}
|
|
}
|
|
}
|
|
var own []struct{ Permissions models.PermissionSet }
|
|
err = fresh(ctx, db).
|
|
Table("users").
|
|
Select("users.permissions").
|
|
Where("users.id = ?", userID).
|
|
Scan(&own).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range own {
|
|
for code, value := range row.Permissions {
|
|
merged[code] = value
|
|
}
|
|
}
|
|
return merged, nil
|
|
}
|
|
|
|
// HasPermission reports whether merged holds code, the single-permission
|
|
// case of Winter's User::hasPermission. A permission is held only when the
|
|
// matching value is exactly 1. Wildcards work as in Winter:
|
|
//
|
|
// - code ending in "*" is held when a different stored code starts with the
|
|
// part before the asterisk and has the value 1;
|
|
// - code starting with "*" is held when a different stored code ends with
|
|
// the part after the asterisk and has the value 1;
|
|
// - otherwise code is held when it is stored with the value 1, or when a
|
|
// stored code ending in "*" with the value 1 is a proper prefix of it.
|
|
//
|
|
// A stored code that starts with "*" is not treated as a wildcard; Winter
|
|
// does not treat it as one either.
|
|
func HasPermission(merged map[string]int, code string) bool {
|
|
switch {
|
|
case len(code) > 1 && strings.HasSuffix(code, "*"):
|
|
prefix := strings.TrimSuffix(code, "*")
|
|
for stored, value := range merged {
|
|
if prefix != stored && strings.HasPrefix(stored, prefix) && value == 1 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case len(code) > 1 && strings.HasPrefix(code, "*"):
|
|
suffix := strings.TrimPrefix(code, "*")
|
|
for stored, value := range merged {
|
|
if suffix != stored && strings.HasSuffix(stored, suffix) && value == 1 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for stored, value := range merged {
|
|
if len(stored) > 1 && strings.HasSuffix(stored, "*") {
|
|
prefix := strings.TrimSuffix(stored, "*")
|
|
if prefix != code && strings.HasPrefix(code, prefix) && value == 1 {
|
|
return true
|
|
}
|
|
continue
|
|
}
|
|
if stored == code && value == 1 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UserHasPermission reports whether the user holds the frontend permission
|
|
// code: MergedPermissions followed by HasPermission.
|
|
func UserHasPermission(ctx context.Context, db *gorm.DB, userID uint, code string) (bool, error) {
|
|
merged, err := MergedPermissions(ctx, db, userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return HasPermission(merged, code), nil
|
|
}
|
|
|
|
// FrontendPermissionOptions returns the rows of
|
|
// golem15_user_frontend_permissions ordered by tab, then code, as PHP
|
|
// FrontendPermission::listTabbedPermissions reads them.
|
|
func FrontendPermissionOptions(ctx context.Context, db *gorm.DB) ([]models.FrontendPermission, error) {
|
|
if db == nil {
|
|
return nil, fmt.Errorf("golem15.user: gorm handle is missing")
|
|
}
|
|
rows := []models.FrontendPermission{}
|
|
if err := fresh(ctx, db).Order("tab ASC, code ASC").Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|