Files
Pulse/pkg/auth/policy_evaluator.go
rcourtman 0ddbf37c59 feat(auth): add policy evaluator and SQLite auth manager for RBAC
- Add policy evaluator for fine-grained access control
- Implement SQLite-backed auth manager for user/role persistence
- Support role-based permissions evaluation
2026-01-12 15:20:49 +00:00

205 lines
6.2 KiB
Go

package auth
import (
"context"
"strings"
)
// 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
}
// SetAdminUser implements AdminConfigurable.
// The admin user always has full access regardless of roles.
func (e *PolicyEvaluator) SetAdminUser(username string) {
// Store admin user for bypass - not implemented in this basic version
// The FileManager handles this separately
}
// RBACAuthorizer wraps PolicyEvaluator to implement Authorizer for the RBAC system.
type RBACAuthorizer struct {
evaluator *PolicyEvaluator
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
if a.adminUser != "" && username == a.adminUser {
return true, nil
}
return a.evaluator.Authorize(ctx, action, resource)
}
// AuthorizeWithAttributes checks authorization with ABAC attributes.
func (a *RBACAuthorizer) AuthorizeWithAttributes(ctx context.Context, action string, resource string, attributes map[string]string) (bool, error) {
username := GetUser(ctx)
// Admin user bypass
if a.adminUser != "" && username == a.adminUser {
return true, nil
}
return a.evaluator.AuthorizeWithAttributes(ctx, action, resource, attributes)
}
// SetAdminUser sets the admin user who has full access.
func (a *RBACAuthorizer) SetAdminUser(username string) {
a.adminUser = username
}
// 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)
}