1use super::ImageReference;
4use anyhow::{Context, Result};
5use cap_std_ext::RootDir;
6use cap_std_ext::cap_std;
7use cap_std_ext::cmdext::{CapStdExtCommandExt, CmdFds};
8use containers_image_proxy::oci_spec::image as oci_image;
9use fn_error_context::context;
10use io_lifetimes::OwnedFd;
11use serde::Deserialize;
12use std::io::Read;
13use std::path::Path;
14use std::process::Stdio;
15use std::str::FromStr;
16use tokio::process::Command;
17
18const INSECURE_ACCEPT_ANYTHING: &str = "insecureAcceptAnything";
23
24const POLICY_ENV_VAR: &str = "CONTAINERS_POLICY_JSON";
27
28const SYSTEM_POLICY_PATHS: &[&str] = &[
30 "etc/containers/policy.json",
31 "usr/share/containers/policy.json",
32];
33
34const USER_POLICY_SUFFIX: &str = "containers/policy.json";
36
37fn resolve_policy_path(
53 root: &RootDir,
54 env_override: Option<&Path>,
55 xdg_config_home: Option<&Path>,
56 home: Option<&Path>,
57) -> Result<std::fs::File> {
58 fn strip_abs(p: &Path) -> &Path {
60 p.strip_prefix("/").unwrap_or(p)
61 }
62
63 if let Some(raw) = env_override.filter(|v| !v.as_os_str().is_empty()) {
65 let relative = strip_abs(raw);
66 tracing::debug!("Using policy path from {POLICY_ENV_VAR}: {}", raw.display());
67 return root.open(relative).with_context(|| {
68 format!(
69 "Opening policy file from {POLICY_ENV_VAR}={}",
70 raw.display()
71 )
72 });
73 }
74
75 let user_candidate = if let Some(xdg) = xdg_config_home {
77 Some(strip_abs(xdg).join(USER_POLICY_SUFFIX))
78 } else {
79 home.map(|h| strip_abs(h).join(".config").join(USER_POLICY_SUFFIX))
80 };
81 if let Some(p) = &user_candidate {
82 if let Ok(f) = root.open(p) {
83 tracing::debug!("Using user policy path: {}", p.display());
84 return Ok(f);
85 }
86 }
87
88 for candidate in SYSTEM_POLICY_PATHS {
90 match root.open(candidate) {
91 Ok(f) => {
92 tracing::debug!("Using system policy path: {candidate}");
93 return Ok(f);
94 }
95 Err(e) => {
96 tracing::debug!("Opening {candidate}: {e:?}");
97 continue;
98 }
99 }
100 }
101
102 anyhow::bail!(
103 "No containers policy.json found; \
104 checked ${POLICY_ENV_VAR}, user config dir, and system paths"
105 )
106}
107
108#[derive(Deserialize)]
109struct PolicyEntry {
110 #[serde(rename = "type")]
111 ty: String,
112}
113#[derive(Deserialize)]
114struct ContainerPolicy {
115 default: Option<Vec<PolicyEntry>>,
116}
117
118impl ContainerPolicy {
119 fn is_default_insecure(&self) -> bool {
120 if let Some(default) = self.default.as_deref() {
121 match default.split_first() {
122 Some((v, &[])) => v.ty == INSECURE_ACCEPT_ANYTHING,
123 _ => false,
124 }
125 } else {
126 false
127 }
128 }
129}
130
131pub(crate) fn container_policy_is_default_insecure(root: &cap_std::fs::Dir) -> Result<bool> {
132 let root = &RootDir::new(root, ".").context("Opening RootDir")?;
133 let f = resolve_policy_path(
134 root,
135 std::env::var_os(POLICY_ENV_VAR).as_deref().map(Path::new),
136 std::env::var_os("XDG_CONFIG_HOME")
137 .as_deref()
138 .map(Path::new),
139 std::env::var_os("HOME").as_deref().map(Path::new),
140 )
141 .context("Resolving containers policy path")?;
142 let r = std::io::BufReader::new(f);
143 let policy: ContainerPolicy = serde_json::from_reader(r)?;
144 Ok(policy.is_default_insecure())
145}
146
147pub(crate) fn new_cmd() -> std::process::Command {
149 let mut cmd = std::process::Command::new(bootc_utils::skopeo_bin());
150 cmd.stdin(Stdio::null());
151 cmd
152}
153
154pub(crate) fn spawn(mut cmd: Command) -> Result<tokio::process::Child> {
156 let cmd = cmd.stdin(Stdio::null()).stderr(Stdio::piped());
157 cmd.spawn().context("Failed to exec skopeo")
158}
159
160#[context("Skopeo copy")]
162pub async fn copy(
163 src: &ImageReference,
164 dest: &ImageReference,
165 authfile: Option<&Path>,
166 add_fd: Option<(std::sync::Arc<OwnedFd>, i32)>,
167 progress: bool,
168) -> Result<oci_image::Digest> {
169 let digestfile = tempfile::NamedTempFile::new()?;
170 let mut cmd = new_cmd();
171 cmd.arg("copy");
172 if !progress {
173 cmd.stdout(std::process::Stdio::null());
174 }
175 cmd.arg("--digestfile");
176 cmd.arg(digestfile.path());
177 if let Some((add_fd, n)) = add_fd {
178 let mut fds = CmdFds::new();
179 fds.take_fd_n(add_fd, n);
180 cmd.take_fds(fds);
181 }
182 if let Some(authfile) = authfile {
183 cmd.arg("--authfile");
184 cmd.arg(authfile);
185 }
186 cmd.args(&[src.to_string(), dest.to_string()]);
187 let mut cmd = tokio::process::Command::from(cmd);
188 cmd.kill_on_drop(true);
189 let proc = super::skopeo::spawn(cmd)?;
190 let output = proc.wait_with_output().await?;
191 if !output.status.success() {
192 let stderr = String::from_utf8_lossy(&output.stderr);
193 return Err(anyhow::anyhow!("skopeo failed: {}\n", stderr));
194 }
195 let mut digestfile = digestfile.into_file();
196 let mut r = String::new();
197 digestfile.read_to_string(&mut r)?;
198 Ok(oci_image::Digest::from_str(r.trim())?)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use cap_std_ext::cap_tempfile;
205
206 const DEFAULT_POLICY: &str = indoc::indoc! {r#"
208 {
209 "default": [
210 {
211 "type": "insecureAcceptAnything"
212 }
213 ],
214 "transports":
215 {
216 "docker-daemon":
217 {
218 "": [{"type":"insecureAcceptAnything"}]
219 }
220 }
221 }
222 "#};
223
224 const REASONABLY_LOCKED_DOWN: &str = indoc::indoc! { r#"
226 {
227 "default": [{"type": "reject"}],
228 "transports": {
229 "dir": {
230 "": [{"type": "insecureAcceptAnything"}]
231 },
232 "atomic": {
233 "hostname:5000/myns/official": [
234 {
235 "type": "signedBy",
236 "keyType": "GPGKeys",
237 "keyPath": "/path/to/official-pubkey.gpg"
238 }
239 ]
240 }
241 }
242 }
243 "#};
244
245 #[test]
246 fn policy_is_insecure() {
247 let p: ContainerPolicy = serde_json::from_str(DEFAULT_POLICY).unwrap();
248 assert!(p.is_default_insecure());
249 for &v in &["{}", REASONABLY_LOCKED_DOWN] {
250 let p: ContainerPolicy = serde_json::from_str(v).unwrap();
251 assert!(!p.is_default_insecure());
252 }
253 }
254
255 fn touch(dir: &cap_std::fs::Dir, path: &str) -> (u64, u64) {
258 use cap_std::fs::MetadataExt;
259 if let Some(parent) = Path::new(path).parent() {
260 dir.create_dir_all(parent).unwrap();
261 }
262 dir.write(path, b"{}").unwrap();
263 let m = dir.metadata(path).unwrap();
264 (m.dev(), m.ino())
265 }
266
267 fn file_id(f: &std::fs::File) -> (u64, u64) {
269 use std::os::unix::fs::MetadataExt;
270 let m = f.metadata().unwrap();
271 (m.dev(), m.ino())
272 }
273
274 fn to_root_dir(dir: &cap_std::fs::Dir) -> RootDir {
275 RootDir::new(dir, ".").unwrap()
276 }
277
278 #[test]
279 fn resolve_policy_path_cases() -> Result<()> {
280 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
281 let root = to_root_dir(&td);
282
283 let etc_id = touch(&td, "etc/containers/policy.json");
284 let _usr_id = touch(&td, "usr/share/containers/policy.json");
285
286 let custom = Path::new("/custom/policy.json");
288 assert!(resolve_policy_path(&root, Some(custom), None, None).is_err());
289 let custom_id = touch(&td, "custom/policy.json");
290 let f = resolve_policy_path(&root, Some(custom), None, None)?;
291 assert_eq!(
292 file_id(&f),
293 custom_id,
294 "env var should open the custom file"
295 );
296
297 let f = resolve_policy_path(&root, Some(Path::new("")), None, None)?;
299 assert_eq!(
300 file_id(&f),
301 etc_id,
302 "empty env var should fall through to /etc"
303 );
304
305 let xdg_id = touch(&td, "xdg/containers/policy.json");
307 let f = resolve_policy_path(&root, None, Some(Path::new("/xdg")), None)?;
308 assert_eq!(file_id(&f), xdg_id, "XDG_CONFIG_HOME should win");
309
310 let f = resolve_policy_path(&root, None, Some(Path::new("/xdg-empty")), None)?;
312 assert_eq!(file_id(&f), etc_id, "missing XDG dir should fall through");
313
314 let home_id = touch(&td, "home/.config/containers/policy.json");
316 let f = resolve_policy_path(&root, None, None, Some(Path::new("/home")))?;
317 assert_eq!(file_id(&f), home_id, "HOME fallback should work");
318
319 let f = resolve_policy_path(&root, None, None, None)?;
321 assert_eq!(
322 file_id(&f),
323 etc_id,
324 "/etc should be preferred over /usr/share"
325 );
326
327 let td2 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
329 let root2 = to_root_dir(&td2);
330 let usr2_id = touch(&td2, "usr/share/containers/policy.json");
331 let f = resolve_policy_path(&root2, None, None, None)?;
332 assert_eq!(file_id(&f), usr2_id, "should fall through to /usr/share");
333
334 let td3 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
336 let root3 = to_root_dir(&td3);
337 assert!(resolve_policy_path(&root3, None, None, None).is_err());
338
339 Ok(())
340 }
341
342 fn symlink_absolute(dir: &cap_std::fs::Dir, target: &str, link: &str) {
346 use std::os::fd::AsRawFd;
347 let real_dir = format!("/proc/self/fd/{}", dir.as_raw_fd());
348 let link_path = std::path::PathBuf::from(real_dir).join(link);
349 std::os::unix::fs::symlink(target, &link_path).unwrap();
350 }
351
352 #[test]
353 fn resolve_policy_path_absolute_symlink() -> Result<()> {
354 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
355 let root = to_root_dir(&td);
356
357 let real_id = touch(&td, "real/policy.json");
359
360 td.create_dir_all("etc/containers")?;
363 symlink_absolute(&td, "/real/policy.json", "etc/containers/policy.json");
364
365 let f = resolve_policy_path(&root, None, None, None)?;
366 assert_eq!(
367 file_id(&f),
368 real_id,
369 "absolute symlink should be resolved relative to root"
370 );
371
372 let td2 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
374 let root2 = to_root_dir(&td2);
375 td2.create_dir_all("etc/containers")?;
376 symlink_absolute(
377 &td2,
378 "/nonexistent/policy.json",
379 "etc/containers/policy.json",
380 );
381 let fallback_id = touch(&td2, "usr/share/containers/policy.json");
382 let f = resolve_policy_path(&root2, None, None, None)?;
383 assert_eq!(
384 file_id(&f),
385 fallback_id,
386 "broken symlink should fall through to next candidate"
387 );
388
389 Ok(())
390 }
391}