1use super::ImageReference;
4use anyhow::{Context, Result};
5use cap_std_ext::cap_std;
6use cap_std_ext::cmdext::{CapStdExtCommandExt, CmdFds};
7use containers_image_proxy::oci_spec::image as oci_image;
8use fn_error_context::context;
9use io_lifetimes::OwnedFd;
10use serde::Deserialize;
11use std::io::Read;
12use std::path::Path;
13use std::process::Stdio;
14use std::str::FromStr;
15use tokio::process::Command;
16
17const INSECURE_ACCEPT_ANYTHING: &str = "insecureAcceptAnything";
22
23const POLICY_ENV_VAR: &str = "CONTAINERS_POLICY_JSON";
26
27const SYSTEM_POLICY_PATHS: &[&str] = &[
29 "etc/containers/policy.json",
30 "usr/share/containers/policy.json",
31];
32
33const USER_POLICY_SUFFIX: &str = "containers/policy.json";
35
36fn resolve_policy_path(
50 root: &cap_std::fs::Dir,
51 env_override: Option<&Path>,
52 xdg_config_home: Option<&Path>,
53 home: Option<&Path>,
54) -> Result<cap_std::fs::File> {
55 fn strip_abs(p: &Path) -> &Path {
57 p.strip_prefix("/").unwrap_or(p)
58 }
59
60 if let Some(raw) = env_override.filter(|v| !v.as_os_str().is_empty()) {
62 let relative = strip_abs(raw);
63 tracing::debug!("Using policy path from {POLICY_ENV_VAR}: {}", raw.display());
64 return root.open(relative).with_context(|| {
65 format!(
66 "Opening policy file from {POLICY_ENV_VAR}={}",
67 raw.display()
68 )
69 });
70 }
71
72 let user_candidate = if let Some(xdg) = xdg_config_home {
74 Some(strip_abs(xdg).join(USER_POLICY_SUFFIX))
75 } else {
76 home.map(|h| strip_abs(h).join(".config").join(USER_POLICY_SUFFIX))
77 };
78 if let Some(p) = &user_candidate {
79 if let Ok(f) = root.open(p) {
80 tracing::debug!("Using user policy path: {}", p.display());
81 return Ok(f);
82 }
83 }
84
85 for candidate in SYSTEM_POLICY_PATHS {
87 if let Ok(f) = root.open(candidate) {
88 tracing::debug!("Using system policy path: {candidate}");
89 return Ok(f);
90 }
91 }
92
93 anyhow::bail!(
94 "No containers policy.json found; \
95 checked ${POLICY_ENV_VAR}, user config dir, and system paths"
96 )
97}
98
99#[derive(Deserialize)]
100struct PolicyEntry {
101 #[serde(rename = "type")]
102 ty: String,
103}
104#[derive(Deserialize)]
105struct ContainerPolicy {
106 default: Option<Vec<PolicyEntry>>,
107}
108
109impl ContainerPolicy {
110 fn is_default_insecure(&self) -> bool {
111 if let Some(default) = self.default.as_deref() {
112 match default.split_first() {
113 Some((v, &[])) => v.ty == INSECURE_ACCEPT_ANYTHING,
114 _ => false,
115 }
116 } else {
117 false
118 }
119 }
120}
121
122pub(crate) fn container_policy_is_default_insecure(root: &cap_std::fs::Dir) -> Result<bool> {
123 let f = resolve_policy_path(
124 root,
125 std::env::var_os(POLICY_ENV_VAR).as_deref().map(Path::new),
126 std::env::var_os("XDG_CONFIG_HOME")
127 .as_deref()
128 .map(Path::new),
129 std::env::var_os("HOME").as_deref().map(Path::new),
130 )
131 .context("Resolving containers policy path")?;
132 let r = std::io::BufReader::new(f);
133 let policy: ContainerPolicy = serde_json::from_reader(r)?;
134 Ok(policy.is_default_insecure())
135}
136
137pub(crate) fn new_cmd() -> std::process::Command {
139 let mut cmd = std::process::Command::new(bootc_utils::skopeo_bin());
140 cmd.stdin(Stdio::null());
141 cmd
142}
143
144pub(crate) fn spawn(mut cmd: Command) -> Result<tokio::process::Child> {
146 let cmd = cmd.stdin(Stdio::null()).stderr(Stdio::piped());
147 cmd.spawn().context("Failed to exec skopeo")
148}
149
150#[context("Skopeo copy")]
152pub async fn copy(
153 src: &ImageReference,
154 dest: &ImageReference,
155 authfile: Option<&Path>,
156 add_fd: Option<(std::sync::Arc<OwnedFd>, i32)>,
157 progress: bool,
158) -> Result<oci_image::Digest> {
159 let digestfile = tempfile::NamedTempFile::new()?;
160 let mut cmd = new_cmd();
161 cmd.arg("copy");
162 if !progress {
163 cmd.stdout(std::process::Stdio::null());
164 }
165 cmd.arg("--digestfile");
166 cmd.arg(digestfile.path());
167 if let Some((add_fd, n)) = add_fd {
168 let mut fds = CmdFds::new();
169 fds.take_fd_n(add_fd, n);
170 cmd.take_fds(fds);
171 }
172 if let Some(authfile) = authfile {
173 cmd.arg("--authfile");
174 cmd.arg(authfile);
175 }
176 cmd.args(&[src.to_string(), dest.to_string()]);
177 let mut cmd = tokio::process::Command::from(cmd);
178 cmd.kill_on_drop(true);
179 let proc = super::skopeo::spawn(cmd)?;
180 let output = proc.wait_with_output().await?;
181 if !output.status.success() {
182 let stderr = String::from_utf8_lossy(&output.stderr);
183 return Err(anyhow::anyhow!("skopeo failed: {}\n", stderr));
184 }
185 let mut digestfile = digestfile.into_file();
186 let mut r = String::new();
187 digestfile.read_to_string(&mut r)?;
188 Ok(oci_image::Digest::from_str(r.trim())?)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use cap_std_ext::cap_tempfile;
195
196 const DEFAULT_POLICY: &str = indoc::indoc! {r#"
198 {
199 "default": [
200 {
201 "type": "insecureAcceptAnything"
202 }
203 ],
204 "transports":
205 {
206 "docker-daemon":
207 {
208 "": [{"type":"insecureAcceptAnything"}]
209 }
210 }
211 }
212 "#};
213
214 const REASONABLY_LOCKED_DOWN: &str = indoc::indoc! { r#"
216 {
217 "default": [{"type": "reject"}],
218 "transports": {
219 "dir": {
220 "": [{"type": "insecureAcceptAnything"}]
221 },
222 "atomic": {
223 "hostname:5000/myns/official": [
224 {
225 "type": "signedBy",
226 "keyType": "GPGKeys",
227 "keyPath": "/path/to/official-pubkey.gpg"
228 }
229 ]
230 }
231 }
232 }
233 "#};
234
235 #[test]
236 fn policy_is_insecure() {
237 let p: ContainerPolicy = serde_json::from_str(DEFAULT_POLICY).unwrap();
238 assert!(p.is_default_insecure());
239 for &v in &["{}", REASONABLY_LOCKED_DOWN] {
240 let p: ContainerPolicy = serde_json::from_str(v).unwrap();
241 assert!(!p.is_default_insecure());
242 }
243 }
244
245 fn touch(dir: &cap_std::fs::Dir, path: &str) -> (u64, u64) {
248 use cap_std::fs::MetadataExt;
249 if let Some(parent) = Path::new(path).parent() {
250 dir.create_dir_all(parent).unwrap();
251 }
252 dir.write(path, b"{}").unwrap();
253 let m = dir.metadata(path).unwrap();
254 (m.dev(), m.ino())
255 }
256
257 fn file_id(f: &cap_std::fs::File) -> (u64, u64) {
259 use cap_std::fs::MetadataExt;
260 let m = f.metadata().unwrap();
261 (m.dev(), m.ino())
262 }
263
264 #[test]
265 fn resolve_policy_path_cases() -> Result<()> {
266 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
267
268 let etc_id = touch(&td, "etc/containers/policy.json");
269 let _usr_id = touch(&td, "usr/share/containers/policy.json");
270
271 let custom = Path::new("/custom/policy.json");
273 assert!(resolve_policy_path(&td, Some(custom), None, None).is_err());
274 let custom_id = touch(&td, "custom/policy.json");
275 let f = resolve_policy_path(&td, Some(custom), None, None)?;
276 assert_eq!(
277 file_id(&f),
278 custom_id,
279 "env var should open the custom file"
280 );
281
282 let f = resolve_policy_path(&td, Some(Path::new("")), None, None)?;
284 assert_eq!(
285 file_id(&f),
286 etc_id,
287 "empty env var should fall through to /etc"
288 );
289
290 let xdg_id = touch(&td, "xdg/containers/policy.json");
292 let f = resolve_policy_path(&td, None, Some(Path::new("/xdg")), None)?;
293 assert_eq!(file_id(&f), xdg_id, "XDG_CONFIG_HOME should win");
294
295 let f = resolve_policy_path(&td, None, Some(Path::new("/xdg-empty")), None)?;
297 assert_eq!(file_id(&f), etc_id, "missing XDG dir should fall through");
298
299 let home_id = touch(&td, "home/.config/containers/policy.json");
301 let f = resolve_policy_path(&td, None, None, Some(Path::new("/home")))?;
302 assert_eq!(file_id(&f), home_id, "HOME fallback should work");
303
304 let f = resolve_policy_path(&td, None, None, None)?;
306 assert_eq!(
307 file_id(&f),
308 etc_id,
309 "/etc should be preferred over /usr/share"
310 );
311
312 let td2 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
314 let usr2_id = touch(&td2, "usr/share/containers/policy.json");
315 let f = resolve_policy_path(&td2, None, None, None)?;
316 assert_eq!(file_id(&f), usr2_id, "should fall through to /usr/share");
317
318 let td3 = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
320 assert!(resolve_policy_path(&td3, None, None, None).is_err());
321
322 Ok(())
323 }
324}