1use crate::{
2 AI_MODELS, ALLOWED_OUTBOUND_HOSTS, CAPABILITY_SETS, ENVIRONMENT, FILES, InheritConfiguration,
3 KEY_VALUE_STORES, SQLITE_DATABASES, VARIABLES,
4};
5use wac_graph::types::{ItemKind, SubtypeChecker, are_semver_compatible};
6use wac_graph::{CompositionGraph, types::Package};
7
8const SPIN_DENY_ADAPTER_BYTES: &[u8] = include_bytes!("../deny_adapter.wasm");
9
10pub fn apply_deny_adapter(
28 source: &[u8],
29 inherits: InheritConfiguration,
30) -> anyhow::Result<Vec<u8>> {
31 let allow = allow_list(inherits);
32
33 let mut graph = CompositionGraph::new();
34
35 let dependency_package = Package::from_bytes("dependency", None, source, graph.types_mut())?;
36
37 let dependency_id = graph.register_package(dependency_package)?;
38
39 let deny_adapter_package = Package::from_bytes(
40 "spin-deny-all-adapter",
41 None,
42 SPIN_DENY_ADAPTER_BYTES,
43 graph.types_mut(),
44 )?;
45
46 let deny_adapter_id = graph.register_package(deny_adapter_package)?;
47
48 let socket_instantiation = graph.instantiate(dependency_id);
50
51 let types = graph.types();
52 let adapter_exports = &types[graph[deny_adapter_id].ty()].exports;
53
54 let mut plug_exports: Vec<(String, String)> = Vec::new();
55 let mut cache = Default::default();
56 let mut checker = SubtypeChecker::new(&mut cache);
57 for (import_name, socket_ty) in &types[graph[dependency_id].ty()].imports {
58 let ItemKind::Instance(iface) = socket_ty else {
59 continue;
60 };
61
62 let iface_name = types[*iface].id.as_deref().unwrap_or(import_name);
64
65 if allow
67 .iter()
68 .any(|allowed| are_semver_compatible(allowed, iface_name))
69 {
70 continue;
71 }
72
73 let matching_export = adapter_exports
74 .get(iface_name)
75 .map(|ty| (iface_name, ty))
76 .or_else(|| {
77 adapter_exports
78 .iter()
79 .find(|(export_name, _)| are_semver_compatible(export_name, iface_name))
80 .map(|(export_name, ty)| (export_name.as_str(), ty))
81 });
82
83 if let Some((plug_name, plug_ty)) = matching_export
84 && checker
85 .is_subtype(*plug_ty, types, *socket_ty, types)
86 .is_ok()
87 {
88 plug_exports.push((plug_name.to_owned(), import_name.clone()));
89 }
90 }
91
92 if plug_exports.is_empty() {
93 return Ok(source.to_vec());
95 }
96
97 let plug_instantiation = graph.instantiate(deny_adapter_id);
98 for (plug_name, socket_name) in plug_exports {
99 let export = graph.alias_instance_export(plug_instantiation, &plug_name)?;
100 graph.set_instantiation_argument(socket_instantiation, &socket_name, export)?;
101 }
102
103 for name in graph.types()[graph[dependency_id].ty()]
105 .exports
106 .keys()
107 .cloned()
108 .collect::<Vec<_>>()
109 {
110 let export = graph.alias_instance_export(socket_instantiation, &name)?;
111 graph.export(export, &name)?;
112 }
113
114 let bytes = graph.encode(Default::default())?;
115 Ok(bytes)
116}
117
118fn allow_list(inherits: InheritConfiguration) -> Vec<&'static str> {
119 let mut allow = vec![];
120
121 match inherits {
122 InheritConfiguration::All => {
123 for (_, capability_set) in CAPABILITY_SETS {
124 allow.extend_from_slice(capability_set);
125 }
126 }
127 InheritConfiguration::Some(inherits) => {
128 for config in inherits {
129 match config.as_str() {
130 "ai_models" => allow.extend_from_slice(AI_MODELS),
131 "allowed_outbound_hosts" => allow.extend_from_slice(ALLOWED_OUTBOUND_HOSTS),
132 "environment" => allow.extend_from_slice(ENVIRONMENT),
133 "files" => allow.extend_from_slice(FILES),
134 "key_value_stores" => allow.extend_from_slice(KEY_VALUE_STORES),
135 "sqlite_databases" => allow.extend_from_slice(SQLITE_DATABASES),
136 "variables" => allow.extend_from_slice(VARIABLES),
137 _ => {}
138 }
139 }
140 }
141 InheritConfiguration::None => {}
142 }
143
144 allow
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use std::collections::HashSet;
151 use wac_graph::types::Types;
152
153 const KV: &str = "spin:key-value/key-value@3.0.0";
154 const ENV: &str = "wasi:cli/environment@0.2.6";
155 const ENV_NEWER: &str = "wasi:cli/environment@0.2.9";
160 const SDK_PG4: &str = "spin:postgres/postgres@4.0.0";
161
162 fn component(imports: &[(&str, Option<&str>)]) -> Vec<u8> {
164 let mut wat = String::from("(component\n");
165 for (name, implements) in imports {
166 match implements {
167 Some(iface) => wat.push_str(&format!(
168 " (import \"{name}\" (implements \"{iface}\") (instance))\n"
169 )),
170 None => wat.push_str(&format!(" (import \"{name}\" (instance))\n")),
171 }
172 }
173 wat.push(')');
174 wat::parse_str(&wat).expect("valid WAT")
175 }
176
177 fn remaining_imports(bytes: &[u8]) -> Vec<String> {
180 let mut types = Types::default();
181 let package = Package::from_bytes("out", None, bytes, &mut types).expect("valid component");
182 types[package.ty()].imports.keys().cloned().collect()
183 }
184
185 fn assert_denied(bytes: &[u8], names: &[&str]) {
186 let remaining = remaining_imports(bytes);
187 for name in names {
188 assert!(
189 !remaining.iter().any(|r| r == name),
190 "`{name}` should have been denied"
191 );
192 }
193 }
194
195 fn assert_retained(bytes: &[u8], names: &[&str]) {
196 let remaining = remaining_imports(bytes);
197 for name in names {
198 assert!(
199 remaining.iter().any(|r| r == name),
200 "`{name}` should have been retained"
201 );
202 }
203 }
204
205 fn some(sets: &[&str]) -> InheritConfiguration {
206 InheritConfiguration::Some(sets.iter().map(|s| s.to_string()).collect())
207 }
208
209 #[test]
210 fn plain_import_is_denied() {
211 let source = component(&[(KV, None)]);
212 let out = apply_deny_adapter(&source, InheritConfiguration::None).unwrap();
213 assert_denied(&out, &[KV]);
214 }
215
216 #[test]
217 fn named_import_is_denied() {
218 let source = component(&[("primary", Some(KV))]);
219 let out = apply_deny_adapter(&source, InheritConfiguration::None).unwrap();
220 assert_denied(&out, &["primary"]);
221 }
222
223 #[test]
224 fn named_import_is_allowed_when_inherited() {
225 let source = component(&[("primary", Some(KV))]);
226 let out = apply_deny_adapter(&source, some(&["key_value_stores"])).unwrap();
227 assert_eq!(out, source);
228 }
229
230 #[test]
231 fn multiple_labels_of_same_interface_are_all_denied() {
232 let source = component(&[("primary", Some(KV)), ("backup", Some(KV))]);
233 let out = apply_deny_adapter(&source, InheritConfiguration::None).unwrap();
234 assert_denied(&out, &["primary", "backup"]);
235 }
236
237 #[test]
238 fn mixed_named_imports_are_filtered_by_interface() {
239 let source = component(&[("primary", Some(KV)), ("env", Some(ENV))]);
240 let out = apply_deny_adapter(&source, some(&["key_value_stores"])).unwrap();
241 assert_retained(&out, &["primary"]);
242 assert_denied(&out, &["env"]);
243 }
244
245 #[test]
246 fn plain_and_named_imports_of_same_interface_are_both_denied() {
247 let source = component(&[(KV, None), ("secondary", Some(KV))]);
248 let out = apply_deny_adapter(&source, InheritConfiguration::None).unwrap();
249 assert_denied(&out, &[KV, "secondary"]);
250 }
251
252 #[test]
253 fn unknown_interface_is_left_untouched() {
254 let source = component(&[("thing", Some("example:unknown/iface@1.0.0"))]);
255 let out = apply_deny_adapter(&source, InheritConfiguration::None).unwrap();
256 assert_eq!(out, source);
257 }
258
259 #[test]
261 fn adapter_does_not_import_what_it_exports() {
262 let mut types = Types::default();
263 let package = Package::from_bytes("adapter", None, SPIN_DENY_ADAPTER_BYTES, &mut types)
264 .expect("valid deny adapter");
265 let world = &types[package.ty()];
266 let conflicts: Vec<_> = world
267 .imports
268 .keys()
269 .filter(|import| {
270 world
271 .exports
272 .keys()
273 .any(|export| are_semver_compatible(import, export))
274 })
275 .collect();
276 assert!(
277 conflicts.is_empty(),
278 "deny adapter imports interfaces it also exports: {conflicts:?}"
279 );
280 }
281
282 #[test]
284 fn adapter_imports_only_types_used_by_its_exports() {
285 let mut types = Types::default();
286 let package = Package::from_bytes("adapter", None, SPIN_DENY_ADAPTER_BYTES, &mut types)
287 .expect("valid deny adapter");
288 let world = &types[package.ty()];
289
290 let mut used = HashSet::new();
291 let mut pending: Vec<_> = world
292 .exports
293 .values()
294 .filter_map(|kind| match kind {
295 ItemKind::Instance(id) => Some(*id),
296 _ => None,
297 })
298 .collect();
299 while let Some(id) = pending.pop() {
300 for used_type in types[id].uses.values() {
301 if let Some(name) = &types[used_type.interface].id
302 && used.insert(name.as_str())
303 {
304 pending.push(used_type.interface);
305 }
306 }
307 }
308
309 let unused: Vec<_> = world
310 .imports
311 .keys()
312 .filter(|import| !used.contains(import.as_str()))
313 .collect();
314 assert!(
315 unused.is_empty(),
316 "deny adapter imports interfaces its exports don't use: {unused:?}"
317 );
318 }
319
320 #[test]
321 fn inherit_all_is_passthrough() {
322 let source = component(&[(KV, None), ("primary", Some(KV)), ("env", Some(ENV))]);
323 let out = apply_deny_adapter(&source, InheritConfiguration::All).unwrap();
324 assert_eq!(out, source);
325 }
326
327 #[test]
329 fn plain_import_at_compatible_version_is_allowed_when_inherited() {
330 let source = component(&[(SDK_PG4, None)]);
331 let out = apply_deny_adapter(&source, some(&["allowed_outbound_hosts"])).unwrap();
332 assert_eq!(out, source);
333 }
334
335 #[test]
336 fn named_import_at_compatible_version_is_allowed_when_inherited() {
337 let source = component(&[("env", Some(ENV_NEWER))]);
338 let out = apply_deny_adapter(&source, some(&["environment"])).unwrap();
339 assert_eq!(out, source);
340 }
341
342 #[test]
345 fn compatible_version_from_another_set_is_still_denied() {
346 let source = component(&[("env", Some(ENV_NEWER))]);
347 let out = apply_deny_adapter(&source, some(&["key_value_stores"])).unwrap();
348 assert_denied(&out, &["env"]);
349 }
350}