#[path = "common/http.rs"] mod http_fixture; #[path = "common/registry.rs"] mod registry_fixture; use std::sync::Arc; use avril_core::ModuleLoader; use avril_runtime::authority::SourceAuthority; use avril_runtime::caps::{Capabilities, Category, Initial, PathTarget}; use avril_runtime::modules::ModuleGraph; use avril_runtime::registry::PackageStore; use futures::executor::block_on; use http_fixture::Http; use registry_fixture::Registry; fn caps_with(cwd: &std::path::Path, grants: &[(&str, bool, &str)]) -> Arc { let mut initial = Initial::new(true); for (category, deny, value) in grants { let category: Category = category.parse().unwrap(); initial.grant(category, *deny, value, cwd).unwrap(); } Capabilities::new(initial, cwd.to_owned()) } fn net_host_port(url: &url::Url) -> String { format!( "{}:{}", url.host_str().unwrap(), url.port_or_known_default().unwrap() ) } fn store_with( options: avril_runtime::registry::Options, authority: SourceAuthority, ) -> PackageStore { PackageStore::new_with_authority(registry_fixture::runtime_handle(), options, authority) .unwrap() } fn graph_options(dir: &std::path::Path) -> avril_runtime::registry::Options { avril_runtime::registry::Options { jsr_registry: url::Url::parse("https://example.invalid/").unwrap(), npm_registry: url::Url::parse("https://example.invalid/").unwrap(), cache: dir.join("cache"), lock: dir.join("avril.lock"), project_root: dir.to_owned(), offline: false, frozen: false, update: false, ephemeral_lock: false, vendor: None, } } #[test] fn execute_keeps_redirect_without_authorizing_target() { let http = Http::new(); let target = Http::new(); http.redirect("start", &target.name("nested/mod.ts")); target.source("nested/mod.ts", "export const value = 7;"); let name = http.name("start"); let dir = tempfile::tempdir().unwrap(); let server = Registry::new(); let caps = caps_with( dir.path(), &[ ("js-exec", false, &name), ("js-exec", true, target.url.as_str()), ], ); let store = store_with(server.options(dir.path()), SourceAuthority::execute(caps)); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 7;" ); } #[test] fn read_checks_network_for_redirect_targets() { let http = Http::new(); let target = Http::new(); http.redirect("start", &target.name("nested/mod.ts")); target.source("nested/mod.ts", "export const value = 7;"); let name = http.name("start"); let http_url = url::Url::parse(&name).unwrap(); // Net for the requested host only: redirect target denies the read. let dir = tempfile::tempdir().unwrap(); let server = Registry::new(); let lock = dir.path().join("avril.lock").to_string_lossy().into_owned(); let only_first = caps_with( dir.path(), &[ ("net", false, &net_host_port(&http_url)), ("read", false, &lock), ], ); let store = store_with( server.options(dir.path()), SourceAuthority::read(only_first), ); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!(target.requests.lock().unwrap().is_empty()); // Net for both hosts: the read succeeds. let dir2 = tempfile::tempdir().unwrap(); let server2 = Registry::new(); let lock2 = dir2 .path() .join("avril.lock") .to_string_lossy() .into_owned(); let both = caps_with( dir2.path(), &[ ("net", false, &net_host_port(&http_url)), ("net", false, &net_host_port(&target.url)), ("read", false, &lock2), ], ); let store = store_with(server2.options(dir2.path()), SourceAuthority::read(both)); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 7;" ); } #[test] fn execution_only_authority_never_exposes_source_in_read() { let http = Http::new(); http.source("mod.ts", "export const value = 1;"); let name = http.name("mod.ts"); let http_url = url::Url::parse(&name).unwrap(); let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("local.ts"), "export const value = 2;").unwrap(); let local = dir.path().join("local.ts").to_string_lossy().into_owned(); // js-exec without net/read: Execute allows, Read denies everything. // The lock grant lets the Read store construct; fetches still deny. let lock = dir.path().join("avril.lock").to_string_lossy().into_owned(); let caps = caps_with( dir.path(), &[ ("js-exec", false, &name), ("js-exec", false, &local), ("read", false, &lock), ], ); let execute = SourceAuthority::execute(caps.clone()); let read = SourceAuthority::read(caps); assert!(execute.check_remote_module(&http_url).is_ok()); assert!(read.check_remote_module(&http_url).is_err()); let target = PathTarget::resolve(&local).unwrap(); assert!(execute.check_local(&target).is_ok()); assert!(read.check_local(&target).is_err()); // Store-level: Read fetch fails even though Execute would allow it. let server = Registry::new(); let store = store_with(server.options(dir.path()), read); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn read_reauthorizes_cached_access_by_final_url() { let http = Http::new(); let target = Http::new(); http.redirect("start", &target.name("nested/mod.ts")); target.source("nested/mod.ts", "export const value = 7;"); let name = http.name("start"); let http_url = url::Url::parse(&name).unwrap(); let dir = tempfile::tempdir().unwrap(); let server = Registry::new(); let lock = dir.path().join("avril.lock").to_string_lossy().into_owned(); let caps = caps_with( dir.path(), &[ ("net", false, &net_host_port(&http_url)), ("net", false, &net_host_port(&target.url)), ("read", false, &lock), ], ); let store = store_with( server.options(dir.path()), SourceAuthority::read(caps.clone()), ); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 7;" ); // Revoke the final host: the cached artifact must deny on re-read. caps.pledge(vec![( Category::Net, None, vec![net_host_port(&target.url)], )]) .unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn execute_revocation_denies_cached_remote_access() { let http = Http::new(); http.source("mod.ts", "export const value = 1;"); let name = http.name("mod.ts"); let dir = tempfile::tempdir().unwrap(); let server = Registry::new(); let caps = caps_with(dir.path(), &[("js-exec", false, &name)]); let store = store_with( server.options(dir.path()), SourceAuthority::execute(caps.clone()), ); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 1;" ); caps.pledge(vec![(Category::JsExec, None, vec![name.clone()])]) .unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn read_local_requires_read_grant() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("a.ts"), "export default 1;").unwrap(); let path = dir.path().join("a.ts").to_string_lossy().into_owned(); let target = PathTarget::resolve(&path).unwrap(); let read_only = caps_with(dir.path(), &[("read", false, &path)]); assert!( SourceAuthority::read(read_only) .check_local(&target) .is_ok() ); let exec_only = caps_with(dir.path(), &[("js-exec", false, &path)]); assert!( SourceAuthority::read(exec_only) .check_local(&target) .is_err() ); } #[test] fn read_lockfile_needs_read_authority() { let http = Http::new(); http.source("mod.ts", "export const value = 1;"); let dir = tempfile::tempdir().unwrap(); let server = Registry::new(); std::fs::write(dir.path().join("avril.lock"), "not a lockfile").unwrap(); let caps = caps_with(dir.path(), &[("net", false, &net_host_port(&http.url))]); // The malformed lock is never parsed without read authority for it. let error = match PackageStore::new_with_authority( registry_fixture::runtime_handle(), server.options(dir.path()), SourceAuthority::read(caps), ) { Ok(_) => panic!("lockfile without read authority must deny"), Err(error) => error.to_string(), }; assert!(error.contains("PermissionDenied"), "{error}"); } #[test] fn lockfile_rereads_respect_revocation() { let http = Http::new(); http.source("mod.ts", "export default 1;"); let server = Registry::new(); for save_first in [false, true] { let dir = tempfile::tempdir().unwrap(); let lock = dir.path().join("avril.lock"); let caps = caps_with( dir.path(), &[ ("read", false, lock.to_str().unwrap()), ("net", false, &net_host_port(&http.url)), ], ); let store = store_with( server.options(dir.path()), SourceAuthority::read(caps.clone()), ); block_on(store.remote_source(&http.name("mod.ts"))).unwrap(); if save_first { store.save().unwrap(); } caps.pledge(vec![( Category::Read, None, vec![lock.to_str().unwrap().to_owned()], )]) .unwrap(); let malformed = "{ secret: 'LOCK_SECRET'"; std::fs::write(&lock, malformed).unwrap(); let error = store.save().unwrap_err().to_string(); assert!(error.contains("PermissionDenied"), "{error}"); assert!(!error.contains("LOCK_SECRET"), "{error}"); assert_eq!(std::fs::read_to_string(lock).unwrap(), malformed); } } #[test] fn npm_and_jsr_acquisition_follow_source_mode() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export const value = 1;")], &[(".", "./mod.ts")], ); server.publish_npm( "lib", "1.0.0", &[ ("package.json", "{\"name\":\"lib\",\"version\":\"1.0.0\"}"), ("index.js", "export default 1;"), ], ); // Read with net to the registry: tarball fetch, verified reads, and // registry sources all work. let dir = tempfile::tempdir().unwrap(); let net = net_host_port(&server.url); let lock = dir.path().join("avril.lock").to_string_lossy().into_owned(); let caps = caps_with(dir.path(), &[("net", false, &net), ("read", false, &lock)]); let store = store_with( server.options(dir.path()), SourceAuthority::read(caps.clone()), ); let artifact = block_on(store.npm_acquire("npm:lib@^1")).unwrap(); assert_eq!( artifact.read_file(dir.path(), "index.js").unwrap(), b"export default 1;" ); let module = block_on(store.resolve("jsr:@test/pkg@^1")).unwrap(); assert!(module.contains("/1.0.0/")); assert_eq!( block_on(store.source(&module)).unwrap(), "export const value = 1;" ); // Revoking the registry denies settled acquisitions on reuse. caps.pledge(vec![(Category::Net, None, vec![net])]).unwrap(); assert!( block_on(store.npm_acquire("npm:lib@^1")) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!( block_on(store.source(&module)) .unwrap_err() .to_string() .contains("PermissionDenied") ); // Execution-only grants never satisfy Read acquisition. let dir2 = tempfile::tempdir().unwrap(); let lock2 = dir2 .path() .join("avril.lock") .to_string_lossy() .into_owned(); let exec_caps = caps_with( dir2.path(), &[ ( "js-exec", false, &format!("{}@test/pkg/", server.url.as_str()), ), ("js-exec", false, &format!("{}lib/", server.url.as_str())), ("read", false, &lock2), ], ); let store = store_with( server.options(dir2.path()), SourceAuthority::read(exec_caps), ); assert!( block_on(store.npm_acquire("npm:lib@^1")) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!( block_on(store.resolve("jsr:@test/pkg@^1")) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn nested_local_configs_follow_source_mode() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let dep = root.join("dep"); std::fs::create_dir_all(&dep).unwrap(); std::fs::write( root.join("avril.conf"), "deps: [{from: './dep', name: 'dep'}]", ) .unwrap(); std::fs::write( dep.join("avril.conf"), "name: 'dep', exports: {'.': './mod.ts'}", ) .unwrap(); std::fs::write(dep.join("mod.ts"), "export const value = 1;").unwrap(); std::fs::write(root.join("main.ts"), "import 'dep';").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); let dep_mod = dep.join("mod.ts").to_string_lossy().into_owned(); let root_str = root.to_string_lossy().into_owned(); let lock = root.join("avril.lock").to_string_lossy().into_owned(); // Execute resolves through both configs with exact import mapping. let caps = caps_with(root, &[("js-exec", false, &root_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::execute(caps), graph_options(root), ) .unwrap(); assert_eq!( block_on(graph.prepare(main.clone(), String::new())).unwrap(), main ); let info = graph.info(&main).unwrap(); assert_eq!( info["modules"][&main]["imports"]["dep"], serde_json::json!(dep_mod) ); assert_eq!( block_on(graph.original_source(&dep_mod)).unwrap(), "export const value = 1;" ); // Read with only execution grants exposes nothing. let exec_only = caps_with( root, &[("js-exec", false, &root_str), ("read", false, &lock)], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(exec_only), graph_options(root), ) .unwrap(); assert!( block_on(graph.prepare(main.clone(), String::new())) .unwrap_err() .to_string() .contains("PermissionDenied") ); // Read with read grants resolves the same nesting. let read_caps = caps_with(root, &[("read", false, &root_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(read_caps), graph_options(root), ) .unwrap(); assert_eq!( block_on(graph.prepare(main.clone(), String::new())).unwrap(), main ); let info = graph.info(&main).unwrap(); assert_eq!( info["modules"][&main]["imports"]["dep"], serde_json::json!(dep_mod) ); } #[test] fn module_graph_read_isolates_execution_only_source() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("avril.conf"), "").unwrap(); std::fs::write(root.join("main.ts"), "export const value = 1;").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); let lock = root.join("avril.lock").to_string_lossy().into_owned(); let caps = caps_with(root, &[("js-exec", false, &main), ("read", false, &lock)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::execute(caps.clone()), graph_options(root), ) .unwrap(); block_on(graph.prepare(main.clone(), String::new())).unwrap(); assert_eq!( block_on(graph.original_source(&main)).unwrap(), "export const value = 1;" ); // Same caps in Read mode expose nothing. let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(caps), graph_options(root), ) .unwrap(); assert!( block_on(graph.original_source(&main)) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn redirected_graph_load_reauthorizes_final_host() { let http = Http::new(); let target = Http::new(); http.redirect("start", &target.name("mod.ts")); target.source("mod.ts", "export const value = 7;"); let name = http.name("start"); let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let lock = root.join("avril.lock").to_string_lossy().into_owned(); let caps = caps_with( root, &[ ("net", false, &net_host_port(&http.url)), ("net", false, &net_host_port(&target.url)), ("read", false, &lock), ], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(caps.clone()), graph_options(root), ) .unwrap(); block_on(graph.prepare(name.clone(), String::new())).unwrap(); assert_eq!( block_on(graph.original_source(&name)).unwrap(), "export const value = 7;" ); // Revoking the final host denies the cached record as well as refetch. caps.pledge(vec![( Category::Net, None, vec![net_host_port(&target.url)], )]) .unwrap(); assert!( graph .load(&name) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!( block_on(graph.original_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn child_graph_revocation_denies_despite_parent_store() { let http = Http::new(); http.source("mod.ts", "export const value = 1;"); let name = http.name("mod.ts"); let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let parent = caps_with(root, &[("js-exec", false, &name)]); let store = Arc::new( PackageStore::new_with_authority( registry_fixture::runtime_handle(), graph_options(root), SourceAuthority::execute(parent.clone()), ) .unwrap(), ); let child = Capabilities::inherit(parent.clone()); let graph = ModuleGraph::open_shared(SourceAuthority::execute(child.clone()), store); block_on(graph.prepare(name.clone(), String::new())).unwrap(); assert_eq!( block_on(graph.original_source(&name)).unwrap(), "export const value = 1;" ); // Revoking only the child denies the cached load. child .pledge(vec![(Category::JsExec, None, vec![name.clone()])]) .unwrap(); assert!( graph .load(&name) .unwrap_err() .to_string() .contains("PermissionDenied") ); } #[test] fn nested_remote_dependency_follows_source_mode() { let front = Http::new(); let target = Http::new(); // The requested child config redirects to its final host. front.redirect("dep/avril.conf", &target.name("avril.conf")); let child_url = front.url.join("dep/").unwrap(); let child_mod = target.name("mod.ts"); target.source( "avril.conf", "name: \"dep\"\nexports: {\".\": \"./mod.ts\"}", ); target.source("mod.ts", "export const value = 1;"); let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write( root.join("avril.conf"), format!("deps: [{{from: \"{}\", name: 'dep'}}]", child_url.as_str()), ) .unwrap(); std::fs::write(root.join("main.ts"), "import 'dep';").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); // Read with net for both hosts traverses the nesting. let net_caps = caps_with( root, &[ ("net", false, &net_host_port(&front.url)), ("net", false, &net_host_port(&target.url)), ("read", false, root.to_str().unwrap()), ], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(net_caps.clone()), graph_options(root), ) .unwrap(); assert_eq!( block_on(graph.prepare(main.clone(), String::new())).unwrap(), main ); assert_eq!( block_on(graph.original_source(&child_mod)).unwrap(), "export const value = 1;" ); let requests = ( front.requests.lock().unwrap().clone(), target.requests.lock().unwrap().clone(), ); // Revoking only the final host denies cached and fresh reads alike // while the requested host stays granted. net_caps .pledge(vec![( Category::Net, None, vec![net_host_port(&target.url)], )]) .unwrap(); assert!( graph .load(&child_mod) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!( block_on(graph.original_source(&child_mod)) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!( block_on(graph.initialize_package(&child_mod)) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert_eq!(*front.requests.lock().unwrap(), requests.0); assert_eq!(*target.requests.lock().unwrap(), requests.1); // Local reads do not authorize remote dependencies with execution-only grants. let dir2 = tempfile::tempdir().unwrap(); let root2 = dir2.path(); std::fs::write( root2.join("avril.conf"), format!("deps: [{{from: \"{}\", name: 'dep'}}]", child_url.as_str()), ) .unwrap(); std::fs::write(root2.join("main.ts"), "import 'dep';").unwrap(); let main2 = root2.join("main.ts").to_string_lossy().into_owned(); let exec_only = caps_with( root2, &[ ("js-exec", false, &front.name("dep/avril.conf")), ("js-exec", false, target.url.as_str()), ("read", false, root2.to_str().unwrap()), ], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(exec_only), graph_options(root2), ) .unwrap(); let error = match block_on(graph.prepare(main2, String::new())) { Ok(_) => panic!("execution-only authority must deny nested export"), Err(error) => error.to_string(), }; assert!(error.contains("PermissionDenied: net"), "{error}"); assert_eq!(*front.requests.lock().unwrap(), requests.0); assert_eq!(*target.requests.lock().unwrap(), requests.1); } #[test] fn hidden_malformed_path_dependency_stays_hidden_in_read() { const MARKER: &str = "SECRET_MARKER_9f3c"; let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("avril.conf"), "deps: ['./secret']").unwrap(); std::fs::write(root.join("main.ts"), "import 'lib';").unwrap(); let secret = root.join("secret"); std::fs::create_dir_all(&secret).unwrap(); std::fs::write(secret.join("avril.conf"), format!(": {{{{ {MARKER} oops")).unwrap(); std::fs::write(secret.join("mod.ts"), "export default 1;").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); let root_str = root.to_string_lossy().into_owned(); let secret_str = secret.to_string_lossy().into_owned(); // Read sees the parent but not the hidden dependency: no marker leaks. let caps = caps_with( root, &[("read", false, &root_str), ("read", true, &secret_str)], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(caps), graph_options(root), ) .unwrap(); let error = match block_on(graph.prepare(main.clone(), String::new())) { Ok(_) => panic!("hidden dependency must deny"), Err(error) => error.to_string(), }; assert!(error.contains("PermissionDenied"), "{error}"); assert!(!error.contains(MARKER), "{error}"); // With the dependency granted, its declared name resolves. let dir2 = tempfile::tempdir().unwrap(); let root2 = dir2.path(); std::fs::write(root2.join("avril.conf"), "deps: ['./secret']").unwrap(); std::fs::write(root2.join("main.ts"), "import 'lib';").unwrap(); let secret2 = root2.join("secret"); std::fs::create_dir_all(&secret2).unwrap(); std::fs::write( secret2.join("avril.conf"), "name: 'lib', exports: {'.': './mod.ts'}", ) .unwrap(); std::fs::write(secret2.join("mod.ts"), "export default 1;").unwrap(); let main2 = root2.join("main.ts").to_string_lossy().into_owned(); let root2_str = root2.to_string_lossy().into_owned(); let caps2 = caps_with(root2, &[("read", false, &root2_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(caps2), graph_options(root2), ) .unwrap(); assert_eq!( block_on(graph.prepare(main2.clone(), String::new())).unwrap(), main2 ); let info = graph.info(&main2).unwrap(); assert!(info["modules"][&main2]["imports"]["lib"].is_string()); } #[test] fn local_revocation_denies_cached_config_access() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("avril.conf"), "name: 'app'").unwrap(); std::fs::write(root.join("main.ts"), "export default 1;").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); let root_str = root.to_string_lossy().into_owned(); let conf = root.join("avril.conf").to_string_lossy().into_owned(); let caps = caps_with(root, &[("read", false, &root_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(caps.clone()), graph_options(root), ) .unwrap(); block_on(graph.prepare(main.clone(), String::new())).unwrap(); assert!(graph.info(&main).unwrap()["modules"][&main]["package"].is_object()); let src = root.join("src"); std::fs::create_dir(&src).unwrap(); let nested = src.join("main.ts"); let authority = SourceAuthority::read(caps.clone()); assert_eq!( ModuleGraph::project_root(&authority, nested.to_str().unwrap()).unwrap(), root ); caps.pledge(vec![(Category::Read, None, vec![conf])]) .unwrap(); // The same graph hides the package once its config is revoked. assert!(graph.info(&main).unwrap()["modules"][&main]["package"].is_null()); assert_eq!( ModuleGraph::project_root(&authority, nested.to_str().unwrap()).unwrap(), src ); } #[test] fn graph_factory_builds_consistent_authority() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::write(root.join("avril.conf"), "").unwrap(); std::fs::write(root.join("main.ts"), "export const value = 1;").unwrap(); let main = root.join("main.ts").to_string_lossy().into_owned(); let lock = root.join("avril.lock").to_string_lossy().into_owned(); let root_str = root.to_string_lossy().into_owned(); // Execute opens and prepares with execution grants. let caps = caps_with(root, &[("js-exec", false, &root_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::execute(caps), graph_options(root), ) .unwrap(); assert_eq!( block_on(graph.prepare(main.clone(), String::new())).unwrap(), main ); // Read opens and prepares with read grants. let read_caps = caps_with(root, &[("read", false, &root_str)]); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(read_caps), graph_options(root), ) .unwrap(); assert_eq!( block_on(graph.prepare(main.clone(), String::new())).unwrap(), main ); // Read construction without lock-read authority denies up front. let bare = caps_with(root, &[("net", false, "example.invalid:443")]); match ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(bare), graph_options(root), ) { Ok(_) => panic!("lock without read authority must deny"), Err(error) => assert!(error.to_string().contains("PermissionDenied"), "{error}"), } // Execution-only sources never satisfy a Read graph. let exec_only = caps_with( root, &[("js-exec", false, &root_str), ("read", false, &lock)], ); let graph = ModuleGraph::open( registry_fixture::runtime_handle(), SourceAuthority::read(exec_only), graph_options(root), ) .unwrap(); assert!( block_on(graph.prepare(main.clone(), String::new())) .unwrap_err() .to_string() .contains("PermissionDenied") ); }