1use super::*;
20
21pub const KEY_ALLOW_NESTING: &str = "user.isb.allow-nesting";
23
24pub const WORKSPACE_KEYS: [(&str, &str); 3] = [
29 ("security.nesting", "true"),
30 ("security.syscalls.intercept.mknod", "true"),
31 ("security.syscalls.intercept.setxattr", "true"),
32];
33
34pub fn is_nesting_key(k: &str) -> bool {
36 k == "security.nesting" || k.starts_with("security.syscalls.intercept.")
37}
38
39pub fn allowed(cfg: &Value) -> bool {
41 cfg[KEY_ALLOW_NESTING].as_str() == Some("true")
42}
43
44fn project_keys(on: bool) -> [(&'static str, &'static str); 3] {
46 let (r, flag) = if on { ("allow", "true") } else { ("block", "") };
47 [
48 ("restricted.containers.nesting", r),
49 ("restricted.containers.interception", r),
50 (KEY_ALLOW_NESTING, flag),
51 ]
52}
53
54pub fn check_config(
59 name: &str,
60 org: Option<&OrgId>,
61 config: &BTreeMap<String, String>,
62 workspace: bool,
63) -> Result<()> {
64 let Some(org) = org else {
65 return Ok(());
66 };
67 if workspace {
68 return Ok(());
69 }
70 match config.keys().find(|k| is_nesting_key(k)) {
71 Some(k) => Err(Error::invalid(format!(
72 "{name}: {k} is refused in org {org}: only the org's workspace may nest, and only when a superadmin allows it (docs/concepts/security.md#the-docker-exception)"
73 ))),
74 None => Ok(()),
75 }
76}
77
78pub fn set(base: &Client, org: &OrgId, on: bool) -> Result<()> {
82 let h = host(base);
83 let pp = format!("/1.0/projects/{}", encode_segment(&org.incus_project()));
84 let p = h
85 .get_opt(&pp)?
86 .ok_or_else(|| Error::NotFound(format!("org {org}")))?;
87 let mut cfg = p["config"].clone();
88 for (k, v) in project_keys(on) {
89 cfg[k] = json!(v);
90 }
91 h.mutate(
92 "PUT",
93 &pp,
94 Some(&json!({"description": p["description"], "config": cfg})),
95 &format!(
96 "{} nesting in org {org}",
97 if on { "allow" } else { "block" }
98 ),
99 h.get_timeouts().other,
100 )?;
101 Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn cfg(keys: &[&str]) -> BTreeMap<String, String> {
109 keys.iter()
110 .map(|k| (k.to_string(), "true".into()))
111 .collect()
112 }
113
114 #[test]
115 fn only_a_workspace_may_nest_in_an_org() {
116 let acme = OrgId::new("acme").unwrap();
117 for k in [
118 "security.nesting",
119 "security.syscalls.intercept.mknod",
120 "security.syscalls.intercept.setxattr",
121 "security.syscalls.intercept.mount",
122 ] {
123 let e = check_config("web-1", Some(&acme), &cfg(&[k]), false).unwrap_err();
124 assert!(e.to_string().contains("only the org's workspace"), "{e}");
125 assert!(check_config("workspace", Some(&acme), &cfg(&[k]), true).is_ok());
126 }
127 assert!(check_config("web-1", Some(&acme), &cfg(&["boot.autostart"]), false).is_ok());
128 assert!(check_config("dev", None, &cfg(&["security.nesting"]), false).is_ok());
130 }
131
132 #[test]
133 fn a_spec_asking_for_nesting_in_an_org_is_refused_unless_it_is_the_workspace() {
134 let ids = "root:1000000:1000000000\n".to_string();
135 let host = crate::plan::HostFacts {
136 subids: crate::idmap::SubIds {
137 subuid: ids.clone(),
138 subgid: ids,
139 caller_owned: false,
140 },
141 pools: vec!["default".into()],
142 path_map: None,
143 initial_copy: false,
144 initial_owner: false,
145 incus_version: Some("7.5.1".into()),
146 invoking_ids: (1000, 1000),
147 shared_root: None,
148 org: Some(OrgId::new("acme").unwrap()),
149 registry: None,
150 project: "isb-acme".into(),
151 };
152 let resolve = |s: &crate::spec::SandboxSpec| {
153 crate::plan::resolve(s, &Default::default(), &host, Path::new("/"))
154 };
155 let mut spec = crate::spec::SandboxSpec::new("web-1", "dev-base")
157 .raw_config("security.nesting", "true");
158 let e = resolve(&spec).unwrap_err();
159 assert!(e.to_string().contains("refused in org acme"), "{e}");
160 let intercept = crate::spec::SandboxSpec::new("web-2", "dev-base")
161 .raw_config("security.syscalls.intercept.mknod", "true");
162 assert!(resolve(&intercept).is_err());
163 spec.workspace_nesting = true;
165 let d = resolve(&spec).unwrap();
166 assert_eq!(d.config["security.nesting"], "true");
167 let y = "image: dev-base\nworkspace_nesting: true\n";
169 assert!(serde_yaml_ng::from_str::<crate::spec::SandboxSpec>(y).is_err());
170 let j = json!({"image": "dev-base", "workspace_nesting": true});
171 assert!(serde_json::from_value::<crate::spec::SandboxSpec>(j).is_err());
172 }
173
174 #[test]
175 fn the_setting_opens_and_closes_the_project() {
176 assert_eq!(
177 project_keys(true),
178 [
179 ("restricted.containers.nesting", "allow"),
180 ("restricted.containers.interception", "allow"),
181 (KEY_ALLOW_NESTING, "true"),
182 ]
183 );
184 assert_eq!(project_keys(false)[0].1, "block");
185 assert_eq!(project_keys(false)[1].1, "block");
186 assert!(allowed(&json!({KEY_ALLOW_NESTING: "true"})));
187 assert!(!allowed(&json!({KEY_ALLOW_NESTING: ""})));
188 assert!(!allowed(&json!({})));
189 assert!(WORKSPACE_KEYS.iter().all(|(k, _)| is_nesting_key(k)));
190 }
191}