Files
orca/internal/cli/authactor.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

68 lines
2.5 KiB
Go

// Package cli — authactor.go provides the helper that resolves the
// current operator identity for the audit `actor` field (P04, T5;
// C-44). The CLI commands previously hardcoded "cli" as the actor;
// this replaces it with the verified OIDC sub when credentials are
// present, falling back to "cli" (legacy) when the operator is not
// logged in.
//
// The actor resolution order is:
// 1. The OIDC credentials file (~/.orca/credentials.json) — set by
// `orca auth login`. The Subject field is the OIDC sub.
// 2. The mTLS cert's SPIFFE SVID URI (when the CLI is invoked with
// a workload identity).
// 3. "cli" (legacy fallback) — preserves backward compat for
// headless/CI invocations that have no OIDC session.
//
// R-021: Orca never issues its own credentials; the sub comes from
// the IdP. The credentials file is 0600 and short-lived (refreshable).
package cli
import (
"context"
"log/slog"
"git.cloudinit.dev/coreci/orca/internal/identity"
)
// currentActor resolves the audit actor for the current CLI
// invocation. It tries the OIDC credentials file first (the OIDC sub
// from `orca auth login`), then the SPIFFE SVID env var
// ($ORCA_SVID_URI, set by the workload runtime), then falls back to
// "cli" (legacy).
//
// Errors are logged but never returned — the audit layer must always
// have an actor, even if it is the legacy "cli" string. A future
// phase can make this a hard error when OIDC is mandatory.
func currentActor(ctx context.Context) string {
// Try OIDC credentials.
if creds, err := identity.LoadCredentials(); err == nil && creds != nil && creds.Subject != "" {
return "oidc:" + creds.Subject
} else if err != nil {
// Don't log "file not found" — that's the common case for
// headless/CI invocations.
slog.Debug("audit actor: oidc credentials not loaded",
slog.String("error", err.Error()))
}
// Legacy fallback.
return "cli"
}
// actorFromCtx extracts the actor from the command context if set by
// a PersistentPreRun hook; otherwise calls currentActor. This allows
// tests to inject a known actor via context.
func actorFromCtx(ctx context.Context) string {
if v, ok := ctx.Value(actorCtxKey{}).(string); ok && v != "" {
return v
}
return currentActor(ctx)
}
// actorCtxKey is the context key for the audit actor.
type actorCtxKey struct{}
// withActor returns a context carrying the audit actor. Used by tests
// to inject a known actor without loading credentials.
func withActor(ctx context.Context, actor string) context.Context {
return context.WithValue(ctx, actorCtxKey{}, actor)
}