mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
RuntimeRoleMetadataKey = "runtime_role"
|
|
RuntimeRoleMonitoringCollector = "monitoring-collector"
|
|
RuntimeRoleActionRunner = "action-runner"
|
|
RuntimeRoleLegacyFullTrust = "legacy-full-trust"
|
|
)
|
|
|
|
var monitoringCollectorScopeOrder = []string{
|
|
ScopeAgentReport,
|
|
ScopeAgentConfigRead,
|
|
ScopeDockerReport,
|
|
ScopeKubernetesReport,
|
|
}
|
|
|
|
// MonitoringCollectorScopes returns the complete allowed collector scope
|
|
// vocabulary. Report and config-read are mandatory; provider report scopes are
|
|
// optional because Proxmox-only collectors do not need them.
|
|
func MonitoringCollectorScopes() []string {
|
|
return append([]string(nil), monitoringCollectorScopeOrder...)
|
|
}
|
|
|
|
// CanonicalizeRoleScopes reduces a role credential to its canonical authority.
|
|
// It never adds a scope. Missing mandatory scopes fail closed because silently
|
|
// adding them would be an authority increase and accepting the credential
|
|
// would violate the role contract.
|
|
func CanonicalizeRoleScopes(role string, scopes []string) ([]string, bool, error) {
|
|
role = strings.TrimSpace(role)
|
|
switch role {
|
|
case RuntimeRoleMonitoringCollector:
|
|
present := scopeSet(scopes)
|
|
for _, required := range []string{ScopeAgentReport, ScopeAgentConfigRead} {
|
|
if _, ok := present[required]; !ok {
|
|
return nil, false, fmt.Errorf("monitoring collector credential requires %s", required)
|
|
}
|
|
}
|
|
canonical := make([]string, 0, len(monitoringCollectorScopeOrder))
|
|
for _, allowed := range monitoringCollectorScopeOrder {
|
|
if _, ok := present[allowed]; ok {
|
|
canonical = append(canonical, allowed)
|
|
}
|
|
}
|
|
return canonical, !slices.Equal(scopes, canonical), nil
|
|
|
|
case RuntimeRoleActionRunner:
|
|
present := scopeSet(scopes)
|
|
if _, ok := present[ScopeAgentExec]; !ok {
|
|
return nil, false, fmt.Errorf("action runner credential requires %s", ScopeAgentExec)
|
|
}
|
|
canonical := []string{ScopeAgentExec}
|
|
return canonical, !slices.Equal(scopes, canonical), nil
|
|
|
|
case RuntimeRoleLegacyFullTrust:
|
|
present := scopeSet(scopes)
|
|
if _, ok := present[ScopeAgentExec]; !ok {
|
|
return nil, false, fmt.Errorf("legacy full-trust credential requires %s", ScopeAgentExec)
|
|
}
|
|
return append([]string(nil), scopes...), false, nil
|
|
|
|
case "":
|
|
return append([]string(nil), scopes...), false, nil
|
|
|
|
default:
|
|
return nil, false, fmt.Errorf("unsupported agent credential role %q", role)
|
|
}
|
|
}
|
|
|
|
// ValidateRoleScopes reports whether a role credential already has canonical,
|
|
// bounded authority. It is used at admission even though load-time migration
|
|
// normally repairs excess authority, providing defense against in-memory
|
|
// mutation and stale configuration snapshots.
|
|
func ValidateRoleScopes(role string, scopes []string) error {
|
|
_, _, err := CanonicalizeRoleScopes(role, scopes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if unexpected := UnexpectedScopes(role, scopes); len(unexpected) > 0 {
|
|
return fmt.Errorf("%s credential has non-canonical scopes", strings.TrimSpace(role))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnexpectedScopes returns scopes that are outside the role's allowlist. It is
|
|
// diagnostics-only and never treats missing mandatory scopes as unexpected.
|
|
func UnexpectedScopes(role string, scopes []string) []string {
|
|
var allowed map[string]struct{}
|
|
switch strings.TrimSpace(role) {
|
|
case RuntimeRoleMonitoringCollector:
|
|
allowed = scopeSet(monitoringCollectorScopeOrder)
|
|
case RuntimeRoleActionRunner:
|
|
allowed = scopeSet([]string{ScopeAgentExec})
|
|
default:
|
|
return nil
|
|
}
|
|
unexpected := make([]string, 0)
|
|
seen := make(map[string]struct{})
|
|
for _, scope := range scopes {
|
|
scope = strings.TrimSpace(scope)
|
|
if _, ok := allowed[scope]; ok {
|
|
continue
|
|
}
|
|
if _, ok := seen[scope]; ok {
|
|
continue
|
|
}
|
|
seen[scope] = struct{}{}
|
|
unexpected = append(unexpected, scope)
|
|
}
|
|
slices.Sort(unexpected)
|
|
return unexpected
|
|
}
|
|
|
|
func scopeSet(scopes []string) map[string]struct{} {
|
|
result := make(map[string]struct{}, len(scopes))
|
|
for _, scope := range scopes {
|
|
result[strings.TrimSpace(scope)] = struct{}{}
|
|
}
|
|
return result
|
|
}
|