This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/elevate/policy.go

131 lines
3.0 KiB
Go

// Package elevate implements the elevation broker for running commands
// outside the Landlock sandbox with human-in-the-loop approval.
package elevate
import (
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
)
// Rule represents a single auto-approve rule.
type Rule struct {
Cmd string `yaml:"cmd,omitempty"` // command pattern (glob)
SSH string `yaml:"ssh,omitempty"` // SSH key fingerprint
}
// Match returns true if the rule matches the given command or fingerprint.
func (r Rule) Match(cmd, fingerprint string) bool {
if r.Cmd != "" {
matched, _ := filepath.Match(r.Cmd, cmd)
if matched {
return true
}
// Also try exact match (glob patterns may not cover all cases)
if r.Cmd == cmd {
return true
}
}
if r.SSH != "" && r.SSH == fingerprint {
return true
}
return false
}
// Policy is a set of auto-approve rules.
type Policy struct {
Rules []Rule `yaml:"rules"`
}
// Matches returns true if any rule matches.
func (p *Policy) Matches(cmd, fingerprint string) bool {
for _, r := range p.Rules {
if r.Match(cmd, fingerprint) {
return true
}
}
return false
}
// Add appends a rule and returns true if it was new.
func (p *Policy) Add(r Rule) bool {
for _, existing := range p.Rules {
if existing == r {
return false
}
}
p.Rules = append(p.Rules, r)
return true
}
// Marshal returns the YAML representation.
func (p *Policy) Marshal() ([]byte, error) {
return yaml.Marshal(p)
}
// ParsePolicy parses YAML data into a Policy.
func ParsePolicy(data []byte, p *Policy) error {
return yaml.Unmarshal(data, p)
}
// PolicyStore manages a disk-backed global policy and in-memory session policies.
type PolicyStore struct {
mu sync.RWMutex
global Policy
path string // disk path for global policy
}
// NewPolicyStore loads (or creates) the global policy from disk.
func NewPolicyStore(path string) *PolicyStore {
ps := &PolicyStore{path: path}
data, err := os.ReadFile(path)
if err == nil {
yaml.Unmarshal(data, &ps.global) //nolint:errcheck
}
return ps
}
// Global returns a copy of the global policy.
func (ps *PolicyStore) Global() Policy {
ps.mu.RLock()
defer ps.mu.RUnlock()
cp := Policy{Rules: make([]Rule, len(ps.global.Rules))}
copy(cp.Rules, ps.global.Rules)
return cp
}
// SetGlobal replaces the global policy and saves to disk.
func (ps *PolicyStore) SetGlobal(p Policy) error {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.global = p
return ps.save()
}
// AddGlobal adds a rule to the global policy and saves.
func (ps *PolicyStore) AddGlobal(r Rule) error {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.global.Add(r)
return ps.save()
}
// MatchesGlobal checks if the global policy matches.
func (ps *PolicyStore) MatchesGlobal(cmd, fingerprint string) bool {
ps.mu.RLock()
defer ps.mu.RUnlock()
return ps.global.Matches(cmd, fingerprint)
}
func (ps *PolicyStore) save() error {
data, err := yaml.Marshal(&ps.global)
if err != nil {
return err
}
dir := filepath.Dir(ps.path)
os.MkdirAll(dir, 0700) //nolint:errcheck
return os.WriteFile(ps.path, data, 0600)
}