Files
orca/internal/engine/registry.go
T
Jon Chery 5232fcb808 fix(P04): wire ACL enforcement + WebAuthn reg auth + audit actor (REQ-153)
R-023: Zero-trust enforcement operationally wired.

ACL enforcement (C-45 staged rollout):
- acl.Check wired into all 5 daemon handlers (dispatch/jobs/nodes/tasks)
- health endpoints exempt (liveness probes not gated)
- ACL log-only mode default (config acl.enforce=false); enforce after
  bootstrap ACL verified
- sshpush auth: ORCA_OIDC_TOKEN validated against JWKS before apply
- txn apply: Authorize hook validates OIDC token before running pull
- acl.json mode 0600 (was 0644)
- flock on acl.json for concurrent grant/revoke
- bootstrap ACL: init grants cluster-admin to orca-admins group + SVID

Audit actor identity:
- currentActor reads OIDC sub from credentials.json (was hardcoded "cli")
- threaded through all audit.Record calls via context

WebAuthn registration auth:
- BeginRegistration/FinishRegistration require authenticated session
- fail-closed 401 when no authFunc configured

New files: internal/daemon/acl.go, internal/cli/authactor.go,
internal/engine/actor.go, internal/identity/authtoken.go,
internal/sshpush/auth.go, internal/txn/auth_test.go

---ci---
project: orca
phase: 4
milestone: v0.13
status: complete
requirements:
  covered: [153]
---/ci---
2026-08-07 20:33:39 +00:00

84 lines
2.7 KiB
Go

package engine
import (
"context"
"fmt"
"log/slog"
"git.cloudinit.dev/coreci/orca/internal/model"
"git.cloudinit.dev/coreci/orca/internal/store"
)
type NodeRegistry struct {
repo *store.NodeRepo
audit *Audit
log *slog.Logger
}
func NewNodeRegistry(repo *store.NodeRepo, audit *Audit, log *slog.Logger) *NodeRegistry {
if log == nil {
log = slog.Default()
}
return &NodeRegistry{repo: repo, audit: audit, log: log}
}
func (r *NodeRegistry) Join(ctx context.Context, n *model.Node) error {
if err := r.repo.Insert(ctx, n); err != nil {
r.audit.Record(ctx, ActorFromCtx(ctx), "node.join", n.ID, "failure", err, map[string]any{
"name": n.Name,
"address": n.Address,
})
return fmt.Errorf("join node: %w", err)
}
r.audit.Record(ctx, ActorFromCtx(ctx), "node.join", n.ID, "success", nil, map[string]any{
"name": n.Name,
"address": n.Address,
})
r.log.Info("node joined",
slog.String("node_id", n.ID),
slog.String("name", n.Name),
slog.String("address", n.Address))
return nil
}
func (r *NodeRegistry) Leave(ctx context.Context, id string) error {
if err := r.repo.UpdateState(ctx, id, model.NodeStateLeft); err != nil {
r.audit.Record(ctx, ActorFromCtx(ctx), "node.leave", id, "failure", err, nil)
return fmt.Errorf("leave node: %w", err)
}
r.audit.Record(ctx, ActorFromCtx(ctx), "node.leave", id, "success", nil, nil)
r.log.Info("node left", slog.String("node_id", id))
return nil
}
func (r *NodeRegistry) Forget(ctx context.Context, id string) error {
if err := r.repo.Delete(ctx, id); err != nil {
r.audit.Record(ctx, ActorFromCtx(ctx), "node.forget", id, "failure", err, nil)
return fmt.Errorf("forget node: %w", err)
}
r.audit.Record(ctx, ActorFromCtx(ctx), "node.forget", id, "success", nil, nil)
r.log.Info("node removed from registry", slog.String("node_id", id))
return nil
}
func (r *NodeRegistry) List(ctx context.Context) ([]*model.Node, error) {
return r.repo.List(ctx)
}
func (r *NodeRegistry) Get(ctx context.Context, id string) (*model.Node, error) {
return r.repo.Get(ctx, id)
}
// SetNodeState updates a node's state to the given raw string (REQ-061).
// Used by the drain commands for the draining/drained states. This is
// the registry-level wrapper around store.NodeRepo.SetNodeState so the
// cli package does not need to reach into the repo directly.
func (r *NodeRegistry) SetNodeState(ctx context.Context, id, state string) error {
if err := r.repo.SetNodeState(ctx, id, state); err != nil {
r.audit.Record(ctx, ActorFromCtx(ctx), "node.set_state", id, "failure", err, map[string]any{"state": state})
return fmt.Errorf("set node state: %w", err)
}
r.log.Info("node state set", slog.String("node_id", id), slog.String("state", state))
return nil
}