auc-tool 0.1.0

Machine-local software passkey authenticator exposed through Linux UHID.
Documentation
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use anyhow::{Context, Result, bail};
use auc_tool::application::{ApplicationServer, AucApplication, LocalSessionAuthorizer};
use auc_tool::authenticator::PresenceGate;
use auc_tool::product::{application_agent_info, managed_product};
use auc_tool::release::AucReleaseSource;
use auc_tool::transport::run_uhid;
use auc_tool::vault::Vault;
use capulus::managed::{
    ActivatedListeners, AgentInfo, JobId, ManagedAgent, ManagementServer, ManagementServerOptions,
    RedeployWorker,
};
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "auc-agent", version, about = "Privileged auc system agent")]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    Serve,
    InstallationManifest,
    Install {
        #[arg(long)]
        operator_uid: u32,
    },
    Uninstall {
        #[arg(long)]
        operator_uid: u32,
        #[arg(long)]
        purge_vault: bool,
    },
    RedeployWorker {
        #[arg(long)]
        job: String,
    },
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Command::Serve => {
            require_root()?;
            let listeners = ActivatedListeners::from_environment(&["application", "capulus"])
                .context("failed to adopt auc systemd sockets")?;
            runtime()?.block_on(serve(listeners))
        }
        Command::InstallationManifest => {
            println!("{}", managed_product()?.installation_manifest().to_json()?);
            Ok(())
        }
        Command::Install { operator_uid } => {
            require_root()?;
            runtime()?.block_on(auc_tool::system::install(operator_uid))
        }
        Command::Uninstall {
            operator_uid,
            purge_vault,
        } => {
            require_root()?;
            runtime()?.block_on(auc_tool::system::uninstall(operator_uid, purge_vault))
        }
        Command::RedeployWorker { job } => {
            require_root()?;
            let job = JobId::parse(&job)?;
            runtime()?.block_on(redeploy_worker(job))
        }
    }
}

fn runtime() -> Result<tokio::runtime::Runtime> {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .thread_name("auc-agent")
        .build()
        .context("failed to create the auc async runtime")
}

async fn serve(mut listeners: ActivatedListeners) -> Result<()> {
    let application_listener = listeners.take_tokio("application")?;
    let management_listener = listeners.take_tokio("capulus")?;
    if !listeners.is_empty() {
        bail!("auc-agent retained an unexpected systemd listener");
    }
    let product = Arc::new(managed_product()?);
    let vault = Vault::open()?;
    let presence = PresenceGate::new();
    let device_present = Arc::new(AtomicBool::new(false));
    let application = Arc::new(AucApplication::new(
        vault.clone(),
        presence.clone(),
        Arc::clone(&device_present),
        LocalSessionAuthorizer::connect().await?,
    ));
    let management = Arc::new(ManagedAgent::new(
        Arc::clone(&product),
        Arc::new(AucReleaseSource::new()?),
    )?);
    let application_server = ApplicationServer::new(application_listener, application);
    let management_server = ManagementServer::new(
        management_listener,
        management,
        ManagementServerOptions::default(),
    )?;
    let transport = tokio::task::spawn_blocking(move || run_uhid(vault, presence, device_present));

    tokio::select! {
        result = application_server.run() => result.context("auc application server stopped"),
        result = management_server.run() => result.context("auc management server stopped"),
        result = transport => result
            .context("auc UHID task panicked")?
            .context("auc UHID transport stopped"),
    }
}

async fn redeploy_worker(job: JobId) -> Result<()> {
    let product = Arc::new(managed_product()?);
    RedeployWorker::new(Arc::clone(&product))?
        .run(job, move || application_health(&product))
        .await
}

fn application_health(product: &capulus::managed::ManagedProduct) -> Result<AgentInfo> {
    application_agent_info()
        .with_context(|| format!("{} application health check failed", product.name()))
}

fn require_root() -> Result<()> {
    if rustix::process::geteuid().is_root() {
        Ok(())
    } else {
        bail!("auc-agent must run as root")
    }
}