224 lines
5.9 KiB
Go
224 lines
5.9 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"gorm.io/gorm"
|
|
|
|
"server/internal/auth"
|
|
"server/internal/models"
|
|
)
|
|
|
|
type RoleConfig struct {
|
|
Roles map[string][]string `json:"roles"`
|
|
Permissions map[string][]string `json:"permissions"`
|
|
Endpoints map[string]string `json:"endpoints"`
|
|
}
|
|
|
|
type RoleResolver struct {
|
|
roleClosure map[string]map[string]struct{}
|
|
permMap map[string]map[string]struct{}
|
|
endpointPerm map[string]string
|
|
}
|
|
|
|
func LoadRoleConfig(path string) (*RoleResolver, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read role config: %w", err)
|
|
}
|
|
var cfg RoleConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse role config: %w", err)
|
|
}
|
|
res := &RoleResolver{
|
|
roleClosure: make(map[string]map[string]struct{}),
|
|
permMap: make(map[string]map[string]struct{}),
|
|
endpointPerm: make(map[string]string),
|
|
}
|
|
|
|
for role := range cfg.Roles {
|
|
res.roleClosure[role] = make(map[string]struct{})
|
|
}
|
|
|
|
// Compute role closure (role implies itself).
|
|
var dfs func(string, map[string]struct{})
|
|
dfs = func(role string, seen map[string]struct{}) {
|
|
if _, ok := seen[role]; ok {
|
|
return
|
|
}
|
|
seen[role] = struct{}{}
|
|
if implied, ok := cfg.Roles[role]; ok {
|
|
for _, r := range implied {
|
|
dfs(r, seen)
|
|
}
|
|
}
|
|
}
|
|
for role := range cfg.Roles {
|
|
set := make(map[string]struct{})
|
|
set[role] = struct{}{}
|
|
dfs(role, set)
|
|
res.roleClosure[role] = set
|
|
}
|
|
|
|
// Build permission map including inherited permissions.
|
|
for role := range cfg.Roles {
|
|
res.permMap[role] = make(map[string]struct{})
|
|
}
|
|
for role := range cfg.Roles {
|
|
closure := res.roleClosure[role]
|
|
for implied := range closure {
|
|
for _, p := range cfg.Permissions[implied] {
|
|
res.permMap[role][p] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalise endpoints to "METHOD /path".
|
|
for key, perm := range cfg.Endpoints {
|
|
parts := strings.SplitN(key, " ", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid endpoint key %q", key)
|
|
}
|
|
method := strings.TrimSpace(strings.ToUpper(parts[0]))
|
|
path := strings.TrimSpace(parts[1])
|
|
res.endpointPerm[method+" "+path] = perm
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (r *RoleResolver) HasRole(userRoles []string, required string) bool {
|
|
for _, ur := range userRoles {
|
|
if closure, ok := r.roleClosure[ur]; ok {
|
|
if _, present := closure[required]; present {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *RoleResolver) HasPermission(userRoles []string, perm string) bool {
|
|
for _, ur := range userRoles {
|
|
if perms, ok := r.permMap[ur]; ok {
|
|
if _, present := perms[perm]; present {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *RoleResolver) PermissionForEndpoint(method, path string) (string, bool) {
|
|
key := strings.ToUpper(method) + " " + path
|
|
perm, ok := r.endpointPerm[key]
|
|
return perm, ok
|
|
}
|
|
|
|
func (r *RoleResolver) RoleDefined(role string) bool {
|
|
_, ok := r.roleClosure[role]
|
|
return ok
|
|
}
|
|
|
|
// RequireRole ensures the authenticated user has the specified role (with inheritance).
|
|
func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
|
|
return func(c fiber.Ctx) error {
|
|
claims, ok := auth.ClaimsFromCtx(c)
|
|
if !ok {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
|
}
|
|
|
|
db, err := dbFromCtx(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
|
}
|
|
|
|
if !resolver.HasRole(user.Roles, role) {
|
|
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
|
}
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// RequirePermission ensures the authenticated user has the given permission.
|
|
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
|
|
return func(c fiber.Ctx) error {
|
|
claims, ok := auth.ClaimsFromCtx(c)
|
|
if !ok {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
|
}
|
|
|
|
db, err := dbFromCtx(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
|
}
|
|
|
|
if !resolver.HasPermission(user.Roles, perm) {
|
|
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
|
}
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireEndpointPermission enforces permission mapping defined in role config.
|
|
// If the endpoint is not configured, or mapped to "*", it allows the request.
|
|
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler {
|
|
return func(c fiber.Ctx) error {
|
|
perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
|
|
if !ok || perm == "*" {
|
|
return c.Next()
|
|
}
|
|
|
|
tokenString := c.Get("Auth-Token")
|
|
if tokenString == "" {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
|
|
}
|
|
|
|
claims, err := authService.ValidateAccessToken(tokenString)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
|
}
|
|
c.Locals("authClaims", claims)
|
|
|
|
db, err := dbFromCtx(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
|
}
|
|
|
|
if !resolver.HasPermission(user.Roles, perm) {
|
|
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
|
}
|
|
|
|
return c.Next()
|
|
}
|
|
}
|