feat: frontend permissions and the merged-permission resolver
- 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
This commit is contained in:
133
classes/permissions.go
Normal file
133
classes/permissions.go
Normal file
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user