voa/
commands.rs

1//! Commands to interact with VOA.
2
3use std::{
4    collections::BTreeMap,
5    fmt::Debug,
6    io::stdin,
7    path::{Path, PathBuf},
8};
9
10use log::info;
11use voa_core::{
12    LoadPathList,
13    Verifier,
14    VerifierWriter,
15    Voa,
16    identifiers::{Context, Os, Purpose, Technology},
17};
18use voa_openpgp::{OpenPgpImport, import::destructured::load_from_dir};
19
20use crate::{
21    Error,
22    utils::{DirOrFile, DirOrFileType},
23};
24
25/// Returns a writable VOA load path.
26///
27/// Gathers the list of writable VOA load paths for the calling user and returns the first from the
28/// list.
29/// If `runtime` is `true`, the ephemeral load path of the calling user is selected instead.
30///
31/// # Errors
32///
33/// Returns an error if no [`LoadPath`][voa_core::LoadPath] can be found.
34///
35/// # Examples
36///
37/// ```
38/// use voa::commands::get_writable_load_path;
39///
40/// # fn main() -> Result<(), voa::Error> {
41/// let config_dir = get_writable_load_path(false)?;
42/// let runtime_dir = get_writable_load_path(true)?;
43/// # Ok(())
44/// # }
45/// ```
46pub fn get_writable_load_path(runtime: bool) -> Result<PathBuf, Error> {
47    let load_path_list = LoadPathList::from_effective_user();
48
49    let filter = voa_core::LoadPathFilter {
50        ephemeral: runtime,
51        writable: true,
52    };
53    let load_path = load_path_list
54        .filter(&filter)
55        .first()
56        .cloned()
57        .ok_or(Error::NoLoadPath)?;
58
59    Ok(load_path.path.clone())
60}
61
62/// Returns an implementation of [`VerifierWriter`] from an input.
63///
64/// Depending on `technology`, attempts to load a verifier from file or directory if `input` is a
65/// [`DirOrFile`]. Attempts to load a verifier from [`stdin`] if `input` is [`None`].
66///
67/// # Note
68///
69/// Currently only supports [`Technology::OpenPGP`].
70///
71/// # Errors
72///
73/// Returns an error if
74///
75/// - a verifier cannot be loaded from file/directory or stdin,
76/// - an unsupported `technology` is provided.
77///
78/// # Examples
79///
80/// ```
81/// use std::io::Write;
82///
83/// use tempfile::{NamedTempFile, tempdir};
84/// use voa::commands::load_verifier;
85///
86/// # fn main() -> testresult::TestResult {
87/// // Write a generic OpenPGP certificate to a temporary file.
88/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
89///
90/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
91/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
92/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
93/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
94/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
95/// Xwo=
96/// =Pkv9
97/// -----END PGP PUBLIC KEY BLOCK-----"#;
98/// let mut temp_file = NamedTempFile::new()?;
99/// write!(temp_file, "{cert}")?;
100/// let input_path = temp_file.path();
101///
102/// // Load an OpenPGP verifier from file.
103/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
104///
105/// // Loading a verifier from file for an unknown technology will fail.
106/// assert!(load_verifier(Some(input_path.try_into()?), "foo".parse()?).is_err());
107/// # Ok(())
108/// # }
109/// ```
110pub fn load_verifier(
111    input: Option<DirOrFile>,
112    technology: Technology,
113) -> Result<impl VerifierWriter + Debug, Error> {
114    match technology {
115        Technology::OpenPGP => Ok(if let Some(path) = input {
116            match path.typ {
117                DirOrFileType::Dir => load_from_dir(&path)?,
118                DirOrFileType::File => OpenPgpImport::from_file(&path)?,
119            }
120        } else {
121            OpenPgpImport::from_reader(stdin())?
122        }),
123        technology => Err(Error::UnsupportedTechnology { technology }),
124    }
125}
126
127/// Writes a `verifier` to a VOA hierarchy in a directory.
128///
129/// The [`VerifierWriter`] implementation writes its on-disk representation to a specific location
130/// in a VOA hierarchy in a VOA base path directory based on the `os`, `purpose` and optional
131/// `context` identifier.
132///
133/// # Errors
134///
135/// Returns an error if [`VerifierWriter::write_to_hierarchy`] fails.
136///
137/// # Examples
138///
139/// ```
140/// use std::io::Write;
141///
142/// use tempfile::{NamedTempFile, tempdir};
143/// use voa::commands::{load_verifier, write_verifier_to_hierarchy};
144///
145/// # fn main() -> testresult::TestResult {
146/// // Write a generic OpenPGP certificate to a temporary file.
147/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
148///
149/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
150/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
151/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
152/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
153/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
154/// Xwo=
155/// =Pkv9
156/// -----END PGP PUBLIC KEY BLOCK-----"#;
157/// let mut temp_file = NamedTempFile::new()?;
158/// write!(temp_file, "{cert}")?;
159/// let input_path = temp_file.path();
160/// // Load an OpenPGP verifier from file.
161/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
162/// // Prepare a temporary output directory.
163/// let temp_dir = tempdir()?;
164///
165/// // Write a verifier to a location in a temporary VOA hierarchy.
166/// write_verifier_to_hierarchy(verifier, temp_dir, "os".parse()?, "packages".parse()?, None)?;
167/// # Ok(())
168/// # }
169/// ```
170pub fn write_verifier_to_hierarchy(
171    verifier: impl VerifierWriter,
172    base_path: impl AsRef<Path>,
173    os: Os,
174    purpose: Purpose,
175    context: Option<Context>,
176) -> Result<(), Error> {
177    let base_path = base_path.as_ref();
178    info!("Writing verifier to VOA base path: {base_path:?}");
179    verifier.write_to_hierarchy(base_path, os, purpose, context)?;
180    Ok(())
181}
182
183/// Searches for all verifiers matching a query of VOA identifiers.
184pub fn search_verifiers(
185    os: Os,
186    purpose: Purpose,
187    context: Option<Context>,
188    technology: Option<Technology>,
189) -> Result<BTreeMap<PathBuf, Vec<Verifier>>, Error> {
190    let context = if let Some(context) = context {
191        context
192    } else {
193        Context::Default
194    };
195    let technology = if let Some(technology) = technology {
196        technology
197    } else {
198        Technology::OpenPGP
199    };
200
201    let voa = Voa::new();
202    let verifiers = voa.lookup(os, purpose, context, technology);
203
204    Ok(verifiers)
205}
206
207#[cfg(test)]
208mod tests {
209    use libc::geteuid;
210    use rstest::rstest;
211    use testresult::TestResult;
212
213    use super::*;
214
215    #[rstest]
216    #[case::runtime_dir(true)]
217    #[case::config_dir(false)]
218    fn get_writable_load_path_succeeds(#[case] runtime: bool) -> TestResult {
219        let load_path = get_writable_load_path(runtime)?;
220
221        let euid = unsafe { geteuid() };
222
223        eprintln!("Load path: {load_path:?}");
224        if runtime {
225            assert!(load_path.starts_with("/run"))
226        } else if euid < 1000 {
227            assert_eq!(load_path, PathBuf::from("/etc/voa"))
228        } else {
229            assert!(load_path.ends_with(".config/voa"))
230        }
231
232        Ok(())
233    }
234
235    #[test]
236    fn load_verifier_fails_on_unsupported_technology() -> TestResult {
237        let result = load_verifier(None, Technology::Custom("foo".parse()?));
238        match result {
239            Err(Error::UnsupportedTechnology { .. }) => {}
240            Err(error) => panic!("Did not raise Error::UnsupportedTechnology but {error}"),
241            Ok(verifier) => {
242                panic!("Is expected to fail, but succeeded to load verifier: {verifier:?}")
243            }
244        }
245
246        Ok(())
247    }
248}