Skip to main content

auc_tool/
product.rs

1use std::path::PathBuf;
2use std::thread;
3use std::time::Duration;
4use std::time::Instant;
5
6use anyhow::{Context, Result, anyhow, bail};
7use capulus::managed::{
8    AgentInfo, AgentServiceOptions, ManagedProduct, ManagedProductOptions, ManagedProgramOptions,
9    ManagedRedeployOptions, ManagementClient, ManagementClientOptions, ManagementRequest,
10    ManagementResponse, ServiceHardening, SocketOptions,
11};
12use semver::Version;
13
14pub const APPLICATION_SOCKET_PATH: &str = "/run/auc/agent.sock";
15pub const MANAGEMENT_SOCKET_PATH: &str = "/run/auc/capulus.sock";
16pub const ACCESS_GROUP: &str = "auc";
17const READINESS_RETRY: Duration = Duration::from_secs(1);
18
19pub fn managed_product() -> Result<ManagedProduct> {
20    ManagedProductOptions {
21        product: "auc".to_string(),
22        package: "auc-tool".to_string(),
23        version: Version::parse(env!("CARGO_PKG_VERSION"))
24            .context("auc package version is not semantic")?,
25        program: ManagedProgramOptions {
26            cargo_binary: "auc".to_string(),
27            installed_path: PathBuf::from("/usr/local/bin/auc"),
28            command_prefix: vec!["agent".to_string()],
29        },
30        service: AgentServiceOptions {
31            description: "auc machine-local passkey authenticator".to_string(),
32            command: vec!["serve".to_string()],
33            restart_delay: Duration::from_secs(2),
34            network_required: false,
35            state_directory_mode: 0o700,
36            hardening: ServiceHardening::Strict {
37                read_write_paths: vec![PathBuf::from("/var/lib/auc")],
38                device_allow: vec![PathBuf::from("/dev/uhid")],
39            },
40        },
41        application_socket: SocketOptions {
42            path: PathBuf::from(APPLICATION_SOCKET_PATH),
43            mode: 0o660,
44            group: Some(ACCESS_GROUP.to_string()),
45        },
46        management_socket: SocketOptions {
47            path: PathBuf::from(MANAGEMENT_SOCKET_PATH),
48            mode: 0o660,
49            group: Some(ACCESS_GROUP.to_string()),
50        },
51        redeploy: ManagedRedeployOptions::default(),
52    }
53    .validate()
54    .context("auc managed-product declaration is invalid")
55}
56
57pub fn application_agent_info() -> Result<AgentInfo> {
58    use crate::application::{ApplicationClient, ApplicationRequest, ApplicationResponse};
59
60    match ApplicationClient::new(APPLICATION_SOCKET_PATH).request(ApplicationRequest::Status)? {
61        ApplicationResponse::Status(status) => {
62            if !status.device_present {
63                bail!("auc UHID device is not present");
64            }
65            Ok(AgentInfo {
66                product: status.product,
67                package: status.package,
68                version: status.version,
69                protocol_major: status.protocol_major,
70            })
71        }
72        _ => Err(anyhow!(
73            "auc application health request returned the wrong response"
74        )),
75    }
76}
77
78pub fn wait_until_healthy(expected_version: &Version, timeout: Duration) -> Result<()> {
79    let product = managed_product()?;
80    let mut options = ManagementClientOptions::new(MANAGEMENT_SOCKET_PATH);
81    options.timeout = Duration::from_secs(2);
82    let management = ManagementClient::new(options);
83    let started = Instant::now();
84    let mut last_error = None;
85    while started.elapsed() < timeout {
86        let result = management
87            .request(ManagementRequest::Info)
88            .map_err(anyhow::Error::from)
89            .and_then(|response| match response {
90                ManagementResponse::Info(info) => Ok(info),
91                _ => Err(anyhow!(
92                    "auc management health request returned the wrong response"
93                )),
94            })
95            .and_then(|management| {
96                let application = application_agent_info()?;
97                for info in [&management, &application] {
98                    if info.product != product.name()
99                        || info.package != product.package()
100                        || info.version != expected_version.to_string()
101                        || info.protocol_major != capulus::managed::PROTOCOL_MAJOR
102                    {
103                        bail!("auc agent health identity or version does not match");
104                    }
105                }
106                Ok(())
107            });
108        match result {
109            Ok(()) => return Ok(()),
110            Err(error) => last_error = Some(error),
111        }
112        thread::sleep(READINESS_RETRY);
113    }
114    Err(last_error
115        .unwrap_or_else(|| anyhow!("auc readiness deadline elapsed without a response"))
116        .context("auc did not become healthy on both sockets"))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn manifest_has_one_program_and_two_distinct_sockets() {
125        let product = managed_product().unwrap();
126        assert_eq!(product.service_name(), "auc-agent.service");
127        assert_eq!(product.application_socket_name(), "auc-agent.socket");
128        assert_eq!(product.management_socket_name(), "auc-capulus.socket");
129        assert_ne!(
130            product.application_socket_path(),
131            product.management_socket_path()
132        );
133        product
134            .installation_manifest()
135            .validate(product.name())
136            .unwrap();
137        let binaries = product
138            .installation_manifest()
139            .files
140            .into_iter()
141            .filter(|file| matches!(file, capulus::managed::ManagedFile::Binary { .. }))
142            .count();
143        assert_eq!(binaries, 1);
144        assert_eq!(product.program().cargo_binary(), "auc");
145        assert_eq!(
146            product.program().installed_path(),
147            std::path::Path::new("/usr/local/bin/auc")
148        );
149    }
150}