// 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) }