Files
pulse/pkg/auth/policy_evaluator.go
T
pulse-triage[bot] 7dd5982ace fix(auth): synchronise local admin permissions during setup
First-run setup saved the configured administrator but left the router authorizer on its startup identity. This split Settings capability checks: administrator-only panels remained visible while API Access and Pulse Intelligence were denied until restart. Keep the captured authorizer aligned when setup commits the identity, and clear its prior bypass on successful development reset.

Cover the exact split with real file-backed RBAC policy and session setup, preserve outsider denial and identity replacement, and synchronise concurrent policy reads. Extension implementations still require independent compatibility review; this is not installation or release acceptance.

Change-source: pulse-maintainer
2026-09-10 06:52:38 +01:00

193 lines
5.6 KiB
Go

package auth
import (
"context"
"strings"
"sync"
)
// PolicyEvaluator implements Authorizer with advanced policy evaluation.
// It supports:
// - Deny precedence (deny rules override allow rules)
// - Role inheritance
// - Attribute-based conditions
// - Resource wildcards
type PolicyEvaluator struct {
manager Manager
}
// NewPolicyEvaluator creates a new policy evaluator.
func NewPolicyEvaluator(manager Manager) *PolicyEvaluator {
return &PolicyEvaluator{manager: manager}
}
// Authorize checks if the user in the context can perform the action on the resource.
// Returns true if allowed, false if denied.
func (e *PolicyEvaluator) Authorize(ctx context.Context, action string, resource string) (bool, error) {
return e.AuthorizeWithAttributes(ctx, action, resource, nil)
}
// AuthorizeWithAttributes checks authorization with additional attributes for ABAC.
func (e *PolicyEvaluator) AuthorizeWithAttributes(ctx context.Context, action string, resource string, attributes map[string]string) (bool, error) {
username := GetUser(ctx)
if username == "" {
return false, nil // No user in context = deny
}
// Get all permissions for the user
permissions := e.getUserEffectivePermissions(username)
if len(permissions) == 0 {
return false, nil // No permissions = deny
}
// Filter permissions that match the requested action and resource
matching := e.filterMatching(permissions, action, resource)
if len(matching) == 0 {
return false, nil // No matching permissions = deny
}
// Evaluate conditions and apply deny precedence
return e.evaluateWithDenyPrecedence(matching, username, attributes), nil
}
// getUserEffectivePermissions returns all permissions for a user including inherited ones.
func (e *PolicyEvaluator) getUserEffectivePermissions(username string) []Permission {
// Check if we have an extended manager with inheritance support
if em, ok := e.manager.(ExtendedManager); ok {
roles := em.GetRolesWithInheritance(username)
var allPerms []Permission
for _, role := range roles {
allPerms = append(allPerms, role.Permissions...)
}
return allPerms
}
// Fall back to basic manager
return e.manager.GetUserPermissions(username)
}
// filterMatching returns permissions that match the requested action and resource.
func (e *PolicyEvaluator) filterMatching(permissions []Permission, action, resource string) []Permission {
var matching []Permission
for _, perm := range permissions {
// Check action match
if !MatchesAction(perm.Action, action) {
continue
}
// Check resource match
if !MatchesResource(perm.Resource, resource) {
continue
}
matching = append(matching, perm)
}
return matching
}
// evaluateWithDenyPrecedence evaluates permissions with deny taking precedence.
// Order: explicit deny > explicit allow > implicit deny
func (e *PolicyEvaluator) evaluateWithDenyPrecedence(permissions []Permission, username string, attributes map[string]string) bool {
var allowFound bool
for _, perm := range permissions {
// Check conditions
if !e.evaluateConditions(perm, username, attributes) {
continue // Condition not met, skip this permission
}
// Check effect
effect := perm.GetEffect()
if effect == EffectDeny {
return false // Explicit deny wins immediately
}
if effect == EffectAllow {
allowFound = true
}
}
return allowFound // Return true only if we found an allow
}
// evaluateConditions checks if all conditions in a permission are satisfied.
func (e *PolicyEvaluator) evaluateConditions(perm Permission, username string, attributes map[string]string) bool {
if len(perm.Conditions) == 0 {
return true // No conditions = always matches
}
for key, expectedValue := range perm.Conditions {
// Handle variable substitution
expectedValue = e.substituteVariables(expectedValue, username, attributes)
// Get actual value from attributes
actualValue, exists := attributes[key]
if !exists {
return false // Required attribute missing
}
if actualValue != expectedValue {
return false // Value doesn't match
}
}
return true
}
// substituteVariables replaces ${variable} placeholders in condition values.
func (e *PolicyEvaluator) substituteVariables(value, username string, attributes map[string]string) string {
// Replace ${user} with the current username
value = strings.ReplaceAll(value, "${user}", username)
// Replace ${attr.key} with attribute values
for key, val := range attributes {
value = strings.ReplaceAll(value, "${attr."+key+"}", val)
}
return value
}
// RBACAuthorizer wraps PolicyEvaluator to implement Authorizer for the RBAC system.
type RBACAuthorizer struct {
evaluator *PolicyEvaluator
adminMu sync.RWMutex
adminUser string
}
// NewRBACAuthorizer creates a new RBAC authorizer.
func NewRBACAuthorizer(manager Manager) *RBACAuthorizer {
return &RBACAuthorizer{
evaluator: NewPolicyEvaluator(manager),
}
}
// Authorize checks if the user can perform the action on the resource.
func (a *RBACAuthorizer) Authorize(ctx context.Context, action string, resource string) (bool, error) {
username := GetUser(ctx)
// Admin user bypass
a.adminMu.RLock()
isAdmin := a.adminUser != "" && username == a.adminUser
a.adminMu.RUnlock()
if isAdmin {
return true, nil
}
return a.evaluator.Authorize(ctx, action, resource)
}
// SetAdminUser sets the admin user who has full access.
func (a *RBACAuthorizer) SetAdminUser(username string) {
a.adminMu.Lock()
a.adminUser = username
a.adminMu.Unlock()
}
// AttributeAuthorizer extends Authorizer with attribute-based authorization.
type AttributeAuthorizer interface {
Authorizer
AuthorizeWithAttributes(ctx context.Context, action string, resource string, attributes map[string]string) (bool, error)
}