mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
- Add policy evaluator for fine-grained access control - Implement SQLite-backed auth manager for user/role persistence - Support role-based permissions evaluation
205 lines
6.2 KiB
Go
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)
|
|
}
|