Files
metacrypt/internal/engine/engine.go
Kyle Isom a5bb366558 Allow system accounts to issue certificates
Service tokens from MCIAS have account_type "system" but no roles.
Thread AccountType through CallerInfo and treat system accounts as
users for certificate issuance. This allows services to request
their own TLS certificates without admin credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:07:22 -07:00

403 lines
11 KiB
Go

// Package engine defines the Engine interface and mount registry.
// Phase 1: interface and registry only, no concrete implementations.
package engine
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"regexp"
"strings"
"sync"
"git.wntrmute.dev/kyle/metacrypt/internal/barrier"
)
// EngineType identifies a cryptographic engine type.
type EngineType string
const (
EngineTypeCA EngineType = "ca"
EngineTypeSSHCA EngineType = "sshca"
EngineTypeTransit EngineType = "transit"
EngineTypeUser EngineType = "user"
)
var (
ErrMountExists = errors.New("engine: mount already exists")
ErrMountNotFound = errors.New("engine: mount not found")
ErrUnknownType = errors.New("engine: unknown engine type")
ErrInvalidName = errors.New("engine: invalid name")
)
var validName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)
// ValidateName checks that a user-provided name is safe for use in barrier
// paths. Names must be 1-128 characters, start with an alphanumeric, and
// contain only alphanumerics, dots, hyphens, and underscores.
func ValidateName(name string) error {
if name == "" || len(name) > 128 || !validName.MatchString(name) {
return fmt.Errorf("%w: %q", ErrInvalidName, name)
}
return nil
}
// CallerInfo carries authentication context into engines.
type CallerInfo struct {
Username string
AccountType string // "human" or "system"
Roles []string
IsAdmin bool
}
// IsUser returns true if the caller is a human user with the "user" or
// "admin" role, or a system (service) account. Guest-only humans are excluded.
func (c *CallerInfo) IsUser() bool {
if c.IsAdmin {
return true
}
if c.AccountType == "system" {
return true
}
for _, r := range c.Roles {
if r == "user" {
return true
}
}
return false
}
// PolicyChecker evaluates whether the caller has access to a specific resource.
// Returns the policy effect ("allow" or "deny") and whether a matching rule was found.
// When matched is false, the caller should fall back to default access rules.
type PolicyChecker func(resource, action string) (effect string, matched bool)
// Request is a request to an engine.
type Request struct {
Data map[string]interface{}
CallerInfo *CallerInfo
CheckPolicy PolicyChecker
Operation string
Path string
}
// Response is a response from an engine.
type Response struct {
Data map[string]interface{}
}
// Engine is the interface that all cryptographic engines must implement.
type Engine interface {
// Type returns the engine type.
Type() EngineType
// Initialize sets up the engine for first use.
Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error
// Unseal opens the engine using state from the barrier.
Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error
// Seal closes the engine and zeroizes key material.
Seal() error
// HandleRequest processes a request.
HandleRequest(ctx context.Context, req *Request) (*Response, error)
}
// Factory creates a new engine instance of a given type.
type Factory func() Engine
// Mount represents a mounted engine instance.
type Mount struct {
Engine Engine `json:"-"`
Name string `json:"name"`
Type EngineType `json:"type"`
MountPath string `json:"mount_path"`
}
// Registry manages mounted engine instances.
type Registry struct {
barrier barrier.Barrier
mounts map[string]*Mount
factories map[EngineType]Factory
logger *slog.Logger
mu sync.RWMutex
}
// NewRegistry creates a new engine registry.
func NewRegistry(b barrier.Barrier, logger *slog.Logger) *Registry {
return &Registry{
mounts: make(map[string]*Mount),
factories: make(map[EngineType]Factory),
barrier: b,
logger: logger,
}
}
// RegisterFactory registers a factory for the given engine type.
func (r *Registry) RegisterFactory(t EngineType, f Factory) {
r.mu.Lock()
defer r.mu.Unlock()
r.logger.Debug("registering engine factory", "type", t)
r.factories[t] = f
}
// mountMeta is persisted to the barrier so mounts survive seal/unseal cycles.
type mountMeta struct {
Name string `json:"name"`
Type EngineType `json:"type"`
}
const mountsPrefix = "engine/_mounts/"
// Mount creates and initializes a new engine mount.
func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType, config map[string]interface{}) error {
if err := ValidateName(name); err != nil {
return err
}
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.mounts[name]; exists {
return ErrMountExists
}
factory, ok := r.factories[engineType]
if !ok {
return fmt.Errorf("%w: %s", ErrUnknownType, engineType)
}
r.logger.Debug("mounting engine", "name", name, "type", engineType)
eng := factory()
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
// Create a per-engine DEK in the barrier for this mount.
if aesBarrier, ok := r.barrier.(*barrier.AESGCMBarrier); ok {
dekKeyID := fmt.Sprintf("engine/%s/%s", engineType, name)
if err := aesBarrier.CreateKey(ctx, dekKeyID); err != nil {
return fmt.Errorf("engine: create DEK %q: %w", dekKeyID, err)
}
}
if err := eng.Initialize(ctx, r.barrier, mountPath, config); err != nil {
return fmt.Errorf("engine: initialize %q: %w", name, err)
}
// Persist mount metadata so it can be reloaded after unseal.
meta, err := json.Marshal(mountMeta{Name: name, Type: engineType})
if err != nil {
return fmt.Errorf("engine: marshal mount metadata: %w", err)
}
if err := r.barrier.Put(ctx, mountsPrefix+name+".json", meta); err != nil {
return fmt.Errorf("engine: persist mount metadata: %w", err)
}
r.mounts[name] = &Mount{
Name: name,
Type: engineType,
MountPath: mountPath,
Engine: eng,
}
r.logger.Debug("engine mounted", "name", name, "type", engineType, "mount_path", mountPath)
return nil
}
// GetEngine returns the engine for the given mount name.
func (r *Registry) GetEngine(name string) (Engine, error) {
r.mu.RLock()
defer r.mu.RUnlock()
mount, exists := r.mounts[name]
if !exists {
return nil, ErrMountNotFound
}
return mount.Engine, nil
}
// GetMount returns the mount for the given name.
func (r *Registry) GetMount(name string) (*Mount, error) {
r.mu.RLock()
defer r.mu.RUnlock()
mount, exists := r.mounts[name]
if !exists {
return nil, ErrMountNotFound
}
return mount, nil
}
// Unmount removes and seals an engine mount.
func (r *Registry) Unmount(ctx context.Context, name string) error {
r.mu.Lock()
defer r.mu.Unlock()
mount, exists := r.mounts[name]
if !exists {
return ErrMountNotFound
}
r.logger.Debug("unmounting engine", "name", name, "type", mount.Type)
if err := mount.Engine.Seal(); err != nil {
return fmt.Errorf("engine: seal %q: %w", name, err)
}
// Remove persisted mount metadata.
if err := r.barrier.Delete(ctx, mountsPrefix+name+".json"); err != nil {
return fmt.Errorf("engine: delete mount metadata %q: %w", name, err)
}
delete(r.mounts, name)
r.logger.Debug("engine unmounted", "name", name)
return nil
}
// UnsealAll discovers persisted mounts from the barrier and reloads them into
// memory. It must be called after the barrier is unsealed.
//
// It first loads mounts recorded in engine/_mounts/, then does a discovery
// pass over each registered engine type to pick up any mounts that predate
// the metadata mechanism (one-time migration).
func (r *Registry) UnsealAll(ctx context.Context) error {
r.mu.Lock()
defer r.mu.Unlock()
// Load mounts from persisted metadata.
if err := r.loadFromMetadata(ctx); err != nil {
return err
}
// Discovery pass: scan each registered engine type for mounts that exist
// in the barrier but have no metadata entry yet (pre-migration mounts).
for engineType, factory := range r.factories {
prefix := fmt.Sprintf("engine/%s/", engineType)
paths, err := r.barrier.List(ctx, prefix)
if err != nil {
continue // no entries for this type
}
// Extract unique mount names from paths like "myca/config.json".
seen := make(map[string]bool)
for _, p := range paths {
parts := strings.SplitN(p, "/", 2)
if len(parts) == 0 || parts[0] == "" || parts[0] == "_mounts" {
continue
}
seen[parts[0]] = true
}
for name := range seen {
if _, exists := r.mounts[name]; exists {
continue // already loaded
}
r.logger.Debug("discovered pre-migration engine mount", "name", name, "type", engineType)
eng := factory()
mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name)
if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil {
return fmt.Errorf("engine: unseal (discovered) %q: %w", name, err)
}
// Write metadata so future restarts don't need the discovery pass.
meta, _ := json.Marshal(mountMeta{Name: name, Type: engineType})
_ = r.barrier.Put(ctx, mountsPrefix+name+".json", meta)
r.mounts[name] = &Mount{
Name: name,
Type: engineType,
MountPath: mountPath,
Engine: eng,
}
}
}
return nil
}
// loadFromMetadata loads mounts recorded under engine/_mounts/. Caller must hold r.mu.
func (r *Registry) loadFromMetadata(ctx context.Context) error {
paths, err := r.barrier.List(ctx, mountsPrefix)
if err != nil {
return nil // no metadata written yet
}
for _, p := range paths {
if !strings.HasSuffix(p, ".json") {
continue
}
data, err := r.barrier.Get(ctx, mountsPrefix+p)
if err != nil {
return fmt.Errorf("engine: read mount metadata %q: %w", p, err)
}
var meta mountMeta
if err := json.Unmarshal(data, &meta); err != nil {
return fmt.Errorf("engine: parse mount metadata %q: %w", p, err)
}
factory, ok := r.factories[meta.Type]
if !ok {
return fmt.Errorf("%w: %s (mount %q)", ErrUnknownType, meta.Type, meta.Name)
}
r.logger.Debug("unsealing engine from metadata", "name", meta.Name, "type", meta.Type)
eng := factory()
mountPath := fmt.Sprintf("engine/%s/%s/", meta.Type, meta.Name)
if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil {
return fmt.Errorf("engine: unseal %q: %w", meta.Name, err)
}
r.mounts[meta.Name] = &Mount{
Name: meta.Name,
Type: meta.Type,
MountPath: mountPath,
Engine: eng,
}
}
return nil
}
// ListMounts returns all current mounts.
func (r *Registry) ListMounts() []Mount {
r.mu.RLock()
defer r.mu.RUnlock()
mounts := make([]Mount, 0, len(r.mounts))
for _, m := range r.mounts {
mounts = append(mounts, Mount{
Name: m.Name,
Type: m.Type,
MountPath: m.MountPath,
})
}
return mounts
}
// HandleRequest routes a request to the appropriate engine.
func (r *Registry) HandleRequest(ctx context.Context, mountName string, req *Request) (*Response, error) {
r.mu.RLock()
mount, exists := r.mounts[mountName]
r.mu.RUnlock()
if !exists {
return nil, ErrMountNotFound
}
r.logger.Debug("routing engine request", "mount", mountName, "operation", req.Operation, "path", req.Path)
return mount.Engine.HandleRequest(ctx, req)
}
// SealAll seals all mounted engines.
func (r *Registry) SealAll() error {
r.mu.Lock()
defer r.mu.Unlock()
r.logger.Debug("sealing all engines", "count", len(r.mounts))
for name, mount := range r.mounts {
if err := mount.Engine.Seal(); err != nil {
return fmt.Errorf("engine: seal %q: %w", name, err)
}
}
return nil
}