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