Files
Pulse/pkg/auth/rbac.go
rcourtman b2a6cd0fa3 fix(agent): add FreeBSD platform support to agent download and UI (#1051)
- Add freebsd-amd64 and freebsd-arm64 to normalizeUnifiedAgentArch()
  so the download endpoint serves FreeBSD binaries when requested
- Add FreeBSD/pfSense/OPNsense platform option to agent setup UI
  with note about bash installation requirement
- Add FreeBSD test cases to unified_agent_test.go

Fixes installation on pfSense/OPNsense where users were getting 404
errors because the backend didn't recognize the freebsd-amd64 arch
parameter from install.sh.
2026-01-11 23:51:12 +00:00

199 lines
6.1 KiB
Go

package auth
import (
"context"
"sync"
"time"
)
// Permission defines an access rule for an action on a resource.
type Permission struct {
Action string `json:"action"` // read, write, delete, admin
Resource string `json:"resource"` // nodes, nodes:pve1, settings, *
Effect string `json:"effect,omitempty"` // "allow" (default) or "deny"
Conditions map[string]string `json:"conditions,omitempty"` // ABAC conditions, e.g., {"tag": "production"}
}
// EffectAllow and EffectDeny are the valid values for Permission.Effect
const (
EffectAllow = "allow"
EffectDeny = "deny"
)
// GetEffect returns the effect, defaulting to "allow" if empty
func (p Permission) GetEffect() string {
if p.Effect == "" {
return EffectAllow
}
return p.Effect
}
// Role represents a collection of permissions.
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ParentID string `json:"parentId,omitempty"` // For role inheritance
Permissions []Permission `json:"permissions"`
IsBuiltIn bool `json:"isBuiltIn"`
Priority int `json:"priority,omitempty"` // For conflict resolution (higher = more priority)
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UserRoleAssignment maps a user to one or more roles.
type UserRoleAssignment struct {
Username string `json:"username"`
RoleIDs []string `json:"roleIds"`
UpdatedAt time.Time `json:"updatedAt"`
}
// RBACChangeLog represents an audit entry for RBAC changes.
type RBACChangeLog struct {
ID string `json:"id"`
Action string `json:"action"` // role_created, role_updated, role_deleted, user_assigned, etc.
EntityType string `json:"entityType"` // role, assignment
EntityID string `json:"entityId"` // Role ID or username
OldValue string `json:"oldValue,omitempty"` // JSON of previous state
NewValue string `json:"newValue,omitempty"` // JSON of new state
User string `json:"user,omitempty"` // Who made the change
Timestamp time.Time `json:"timestamp"`
}
// RBAC change action constants
const (
ActionRoleCreated = "role_created"
ActionRoleUpdated = "role_updated"
ActionRoleDeleted = "role_deleted"
ActionUserAssigned = "user_assigned"
ActionUserUnassigned = "user_unassigned"
ActionUserRolesUpdate = "user_roles_updated"
)
// Built-in Role IDs
const (
RoleAdmin = "admin"
RoleOperator = "operator"
RoleViewer = "viewer"
RoleAuditor = "auditor"
RoleCompliance = "compliance" // Alias for Auditor
)
// Manager defines the interface for managing RBAC data.
// This is typically implemented by the enterprise RBAC store.
type Manager interface {
// Role management
GetRoles() []Role
GetRole(id string) (Role, bool)
SaveRole(role Role) error
DeleteRole(id string) error
// Assignment management
GetUserAssignments() []UserRoleAssignment
GetUserAssignment(username string) (UserRoleAssignment, bool)
AssignRole(username string, roleID string) error
UpdateUserRoles(username string, roleIDs []string) error
RemoveRole(username string, roleID string) error
// Effective permissions
GetUserPermissions(username string) []Permission
}
// ExtendedManager extends Manager with advanced RBAC features.
// This is implemented by the SQLite-backed manager for Pro features.
type ExtendedManager interface {
Manager
// Role inheritance
GetRoleWithInheritance(id string) (Role, []Permission, bool) // Returns role and all inherited permissions
GetRolesWithInheritance(username string) []Role // Returns user's roles with inheritance chain
// Change log
GetChangeLogs(limit int, offset int) []RBACChangeLog
GetChangeLogsForEntity(entityType, entityID string) []RBACChangeLog
// Context-aware operations (for audit trail)
SaveRoleWithContext(role Role, username string) error
DeleteRoleWithContext(id string, username string) error
UpdateUserRolesWithContext(username string, roleIDs []string, byUser string) error
}
var (
globalManager Manager
managerMu sync.RWMutex
)
// SetManager sets the global RBAC manager instance.
// This should be called during application initialization.
func SetManager(m Manager) {
managerMu.Lock()
defer managerMu.Unlock()
globalManager = m
}
// GetManager returns the global RBAC manager instance.
func GetManager() Manager {
managerMu.RLock()
defer managerMu.RUnlock()
return globalManager
}
// Requirement helpers
func HasPermission(ctx context.Context, action, resource string) bool {
authorizer := GetAuthorizer()
allowed, _ := authorizer.Authorize(ctx, action, resource)
return allowed
}
// GetExtendedManager returns the global manager as ExtendedManager if it implements the interface.
func GetExtendedManager() ExtendedManager {
managerMu.RLock()
defer managerMu.RUnlock()
if em, ok := globalManager.(ExtendedManager); ok {
return em
}
return nil
}
// MatchesResource checks if a permission's resource pattern matches a requested resource.
// Supports:
// - Exact match: "nodes" matches "nodes"
// - Specific ID: "nodes:pve1" matches "nodes:pve1"
// - Wildcard: "nodes:*" matches "nodes:pve1"
// - Global wildcard: "*" matches any resource
func MatchesResource(pattern, resource string) bool {
// Global wildcard matches everything
if pattern == "*" {
return true
}
// Exact match
if pattern == resource {
return true
}
// Check for wildcard pattern (e.g., "nodes:*")
if len(pattern) > 2 && pattern[len(pattern)-2:] == ":*" {
prefix := pattern[:len(pattern)-2]
// Match if resource starts with the prefix
if resource == prefix {
return true
}
// Match if resource has the prefix followed by a colon
if len(resource) > len(prefix) && resource[:len(prefix)+1] == prefix+":" {
return true
}
}
return false
}
// MatchesAction checks if a permission's action matches a requested action.
// "admin" action matches any action.
func MatchesAction(permAction, requestedAction string) bool {
if permAction == "admin" {
return true
}
return permAction == requestedAction
}