ostree_ext/
globals.rs

1//! Module containing access to global state.
2
3use super::Result;
4use anyhow::Context;
5use camino::{Utf8Path, Utf8PathBuf};
6use cap_std_ext::RootDir;
7use cap_std_ext::cap_std::fs::Dir;
8use fn_error_context::context;
9use ostree::glib;
10use std::fs::File;
11use std::sync::OnceLock;
12
13struct ConfigPaths {
14    persistent: Utf8PathBuf,
15    runtime: Utf8PathBuf,
16    system: Option<Utf8PathBuf>,
17}
18
19/// Get the runtime and persistent config directories.  In the system (root) case, these
20/// system(root) case:  /run/ostree           /etc/ostree        /usr/lib/ostree
21/// user(nonroot) case: /run/user/$uid/ostree ~/.config/ostree   <none>
22fn get_config_paths(root: bool) -> &'static ConfigPaths {
23    if root {
24        static PATHS_ROOT: OnceLock<ConfigPaths> = OnceLock::new();
25        PATHS_ROOT.get_or_init(|| ConfigPaths::new("etc", "run", Some("usr/lib")))
26    } else {
27        static PATHS_USER: OnceLock<ConfigPaths> = OnceLock::new();
28        PATHS_USER.get_or_init(|| {
29            ConfigPaths::new(
30                Utf8PathBuf::try_from(glib::user_config_dir()).unwrap(),
31                Utf8PathBuf::try_from(glib::user_runtime_dir()).unwrap(),
32                None,
33            )
34        })
35    }
36}
37
38impl ConfigPaths {
39    fn new<P: AsRef<Utf8Path>>(persistent: P, runtime: P, system: Option<P>) -> Self {
40        fn relative_owned(p: &Utf8Path) -> Utf8PathBuf {
41            p.as_str().trim_start_matches('/').into()
42        }
43        let mut r = ConfigPaths {
44            persistent: relative_owned(persistent.as_ref()),
45            runtime: relative_owned(runtime.as_ref()),
46            system: system.as_ref().map(|s| relative_owned(s.as_ref())),
47        };
48        let path = "ostree";
49        r.persistent.push(path);
50        r.runtime.push(path);
51        if let Some(system) = r.system.as_mut() {
52            system.push(path);
53        }
54        r
55    }
56
57    /// Return the path and an open fd for a config file, if it exists.
58    pub(crate) fn open_file(
59        &self,
60        root: &RootDir,
61        p: impl AsRef<Utf8Path>,
62    ) -> Result<Option<(Utf8PathBuf, File)>> {
63        let p = p.as_ref();
64        let mut runtime = self.runtime.clone();
65        runtime.push(p);
66        if let Some(f) = root
67            .open_optional(&runtime)
68            .context("Opening runtime auth file")?
69        {
70            return Ok(Some((runtime, f)));
71        }
72        let mut persistent = self.persistent.clone();
73        persistent.push(p);
74        if let Some(f) = root
75            .open_optional(&persistent)
76            .context("Opening persistent auth file")?
77        {
78            return Ok(Some((persistent, f)));
79        }
80        if let Some(mut system) = self.system.clone() {
81            system.push(p);
82            if let Some(f) = root
83                .open_optional(&system)
84                .context("Opening system auth file")?
85            {
86                return Ok(Some((system, f)));
87            }
88        }
89        Ok(None)
90    }
91}
92
93/// Return the path to the global container authentication file, if it exists.
94#[context("Loading global authfile")]
95pub fn get_global_authfile(root: &Dir) -> Result<Option<(Utf8PathBuf, File)>> {
96    let root = &RootDir::new(root, ".").context("Opening RootDir")?;
97    let am_uid0 = rustix::process::getuid() == rustix::process::Uid::ROOT;
98    get_global_authfile_impl(root, am_uid0)
99}
100
101/// Return the path to the global container authentication file, if it exists.
102fn get_global_authfile_impl(root: &RootDir, am_uid0: bool) -> Result<Option<(Utf8PathBuf, File)>> {
103    let paths = get_config_paths(am_uid0);
104    paths.open_file(root, "auth.json")
105}
106
107#[cfg(test)]
108mod tests {
109    use std::io::Read;
110
111    use super::*;
112    use camino::Utf8PathBuf;
113    use cap_std_ext::{cap_std, cap_tempfile};
114
115    fn read_authfile(
116        root: &cap_std_ext::RootDir,
117        am_uid0: bool,
118    ) -> Result<Option<(Utf8PathBuf, String)>> {
119        let r = get_global_authfile_impl(root, am_uid0)?;
120        if let Some((path, mut f)) = r {
121            let mut s = String::new();
122            f.read_to_string(&mut s)?;
123            Ok(Some((path.try_into()?, s)))
124        } else {
125            Ok(None)
126        }
127    }
128
129    #[test]
130    fn test_config_paths() -> Result<()> {
131        let root = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
132        let rootdir = &RootDir::new(root, ".")?;
133        assert!(read_authfile(rootdir, true).unwrap().is_none());
134        root.create_dir_all("etc/ostree")?;
135        root.write("etc/ostree/auth.json", "etc ostree auth")?;
136        let (p, authdata) = read_authfile(rootdir, true).unwrap().unwrap();
137        assert_eq!(p, "etc/ostree/auth.json");
138        assert_eq!(authdata, "etc ostree auth");
139        root.create_dir_all("usr/lib/ostree")?;
140        root.write("usr/lib/ostree/auth.json", "usrlib ostree auth")?;
141        // We should see /etc content still
142        let (p, authdata) = read_authfile(rootdir, true).unwrap().unwrap();
143        assert_eq!(p, "etc/ostree/auth.json");
144        assert_eq!(authdata, "etc ostree auth");
145        // Now remove the /etc content, unveiling the /usr content
146        root.remove_file("etc/ostree/auth.json")?;
147        let (p, authdata) = read_authfile(rootdir, true).unwrap().unwrap();
148        assert_eq!(p, "usr/lib/ostree/auth.json");
149        assert_eq!(authdata, "usrlib ostree auth");
150
151        // Verify symlinks work, both relative...
152        root.create_dir_all("etc/containers")?;
153        root.write("etc/containers/auth.json", "etc containers ostree auth")?;
154        root.symlink_contents("../containers/auth.json", "etc/ostree/auth.json")?;
155        let (p, authdata) = read_authfile(rootdir, true).unwrap().unwrap();
156        assert_eq!(p, "etc/ostree/auth.json");
157        assert_eq!(authdata, "etc containers ostree auth");
158        // And an absolute link
159        root.remove_file("etc/ostree/auth.json")?;
160        root.symlink_contents("/etc/containers/auth.json", "etc/ostree/auth.json")?;
161        assert_eq!(p, "etc/ostree/auth.json");
162        assert_eq!(authdata, "etc containers ostree auth");
163
164        // Non-root
165        let mut user_runtime_dir =
166            Utf8Path::from_path(glib::user_runtime_dir().strip_prefix("/").unwrap())
167                .unwrap()
168                .to_path_buf();
169        user_runtime_dir.push("ostree");
170        root.create_dir_all(&user_runtime_dir)?;
171        user_runtime_dir.push("auth.json");
172        root.write(&user_runtime_dir, "usr_runtime_dir ostree auth")?;
173
174        let mut user_config_dir =
175            Utf8Path::from_path(glib::user_config_dir().strip_prefix("/").unwrap())
176                .unwrap()
177                .to_path_buf();
178        user_config_dir.push("ostree");
179        root.create_dir_all(&user_config_dir)?;
180        user_config_dir.push("auth.json");
181        root.write(&user_config_dir, "usr_config_dir ostree auth")?;
182
183        // We should see runtime_dir content still
184        let (p, authdata) = read_authfile(rootdir, false).unwrap().unwrap();
185        assert_eq!(p, user_runtime_dir);
186        assert_eq!(authdata, "usr_runtime_dir ostree auth");
187
188        // Now remove the runtime_dir content, unveiling the config_dir content
189        root.remove_file(&user_runtime_dir)?;
190        let (p, authdata) = read_authfile(rootdir, false).unwrap().unwrap();
191        assert_eq!(p, user_config_dir);
192        assert_eq!(authdata, "usr_config_dir ostree auth");
193
194        Ok(())
195    }
196}