131 lines
3.0 KiB
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)
|
|
}
|