mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
7dd5982ace
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
193 lines
5.6 KiB
Go
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)
|
|
}
|