Skip to content

Commit

Permalink
feat: support setting network permissions via allowNet option (#386)
Browse files Browse the repository at this point in the history
* feat: support setting network permissions via allowNet option

* stamp: format

* chore: add integration tests for `allowNet` option

* chore: update `Cargo.lock`

---------

Co-authored-by: Nyannyacha <[email protected]>
  • Loading branch information
2 people authored and kallebysantos committed Jul 31, 2024
1 parent 4ff108f commit 4a99525
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 14 deletions.
72 changes: 71 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/base/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ mod supabase_startup_snapshot {
let user_agent = String::from("supabase");
let fs = Arc::new(deno_fs::RealFs);
let extensions: Vec<Extension> = vec![
sb_core_permissions::init_ops_and_esm(false),
sb_core_permissions::init_ops_and_esm(false, None),
deno_webidl::deno_webidl::init_ops_and_esm(),
deno_console::deno_console::init_ops_and_esm(),
deno_url::deno_url::init_ops_and_esm(),
Expand Down
15 changes: 13 additions & 2 deletions crates/base/src/deno_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::collections::HashMap;
use std::ffi::c_void;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use std::task::Poll;
use std::time::Duration;
Expand Down Expand Up @@ -266,13 +267,23 @@ where
}

let mut net_access_disabled = false;
let mut allow_net = None;
let mut allow_remote_modules = true;

if is_user_worker {
let user_conf = conf.as_user_worker().unwrap();

net_access_disabled = user_conf.net_access_disabled;
allow_remote_modules = user_conf.allow_remote_modules;

allow_net = match &user_conf.allow_net {
Some(allow_net) => Some(
allow_net
.iter()
.map(|s| FromStr::from_str(s.as_str()))
.collect::<Result<Vec<_>, _>>()?,
),
None => None,
};
}

let mut maybe_arc_import_map = None;
Expand Down Expand Up @@ -413,7 +424,7 @@ where
let mod_code = module_code;

let extensions = vec![
sb_core_permissions::init_ops(net_access_disabled),
sb_core_permissions::init_ops(net_access_disabled, allow_net),
deno_webidl::deno_webidl::init_ops(),
deno_console::deno_console::init_ops(),
deno_url::deno_url::init_ops(),
Expand Down
15 changes: 15 additions & 0 deletions crates/base/test_cases/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Deno.serve(async (req: Request) => {
try {
const payload = await req.json();
const url = new URL(payload["url"]);
const resp = await fetch(url);
const body = await resp.text();

return Response.json({
status: resp.status,
body
});
} catch (e) {
return new Response(e.toString(), { status: 500 });
}
});
76 changes: 76 additions & 0 deletions crates/base/test_cases/main_with_allow_net/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
console.log('main function started');

Deno.serve({
handler: async (req: Request) => {
console.log(req.url);
const url = new URL(req.url);
const { pathname } = url;
const path_parts = pathname.split("/");
const service_name = path_parts[1];

if (!service_name || service_name === "") {
const error = { msg: "missing function name in request" }
return new Response(
JSON.stringify(error),
{ status: 400, headers: { "Content-Type": "application/json" } },
)
}

const servicePath = `./test_cases/${service_name}`;

let allowNet: string[] | null | undefined;

try {
const payload = await req.clone().json();
allowNet = payload.allowNet;
} catch { }

console.error(`serving the request with ${servicePath}`);

const createWorker = async () => {
const memoryLimitMb = 150;
const workerTimeoutMs = 10 * 60 * 1000;
const cpuTimeSoftLimitMs = 10 * 60 * 1000;
const cpuTimeHardLimitMs = 10 * 60 * 1000;
const noModuleCache = false;
const importMapPath = null;
const envVarsObj = Deno.env.toObject();
const envVars = Object.keys(envVarsObj).map(k => [k, envVarsObj[k]]);

return await EdgeRuntime.userWorkers.create({
servicePath,
memoryLimitMb,
workerTimeoutMs,
cpuTimeSoftLimitMs,
cpuTimeHardLimitMs,
noModuleCache,
importMapPath,
envVars,
allowNet,
});
}

const callWorker = async () => {
try {
const worker = await createWorker();
return await worker.fetch(req);
} catch (e) {
console.error(e);

// if (e instanceof Deno.errors.WorkerRequestCancelled) {
// return await callWorker();
// }

const error = { msg: e.toString() }
return new Response(
JSON.stringify(error),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}

return callWorker();
},

onError: e => new Response(e.toString(), { status: 500 })
})
111 changes: 111 additions & 0 deletions crates/base/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,117 @@ async fn test_should_not_hang_when_forced_redirection_for_specifiers() {
}
}

async fn test_allow_net<F, R>(allow_net: Option<Vec<&str>>, url: &str, callback: F)
where
F: FnOnce(Result<Response, reqwest::Error>) -> R,
R: Future<Output = ()>,
{
let payload = serde_json::json!({
"allowNet": allow_net,
"url": url
});

let client = Client::new();
let req = client
.request(
Method::POST,
format!("http://localhost:{}/fetch", NON_SECURE_PORT),
)
.json(&payload)
.build()
.unwrap();

integration_test!(
"./test_cases/main_with_allow_net",
NON_SECURE_PORT,
"",
None,
None,
Some(RequestBuilder::from_parts(client, req)),
None,
(|resp| async {
callback(resp).await;
}),
TerminationToken::new()
);
}

#[tokio::test]
#[serial]
async fn test_allow_net_fetch_google_com() {
#[derive(Deserialize)]
struct FetchResponse {
status: u16,
body: String,
}

// 1. allow only specific hosts
test_allow_net(
// because google.com redirects to www.google.com
Some(vec!["google.com", "www.google.com"]),
"https://google.com",
|resp| async move {
let resp = resp.unwrap();

assert_eq!(resp.status().as_u16(), StatusCode::OK);

let payload = resp.json::<FetchResponse>().await.unwrap();

assert_eq!(payload.status, StatusCode::OK);
assert!(!payload.body.is_empty());
},
)
.await;

// 2. allow only specific host (but not considering the redirected host)
test_allow_net(
Some(vec!["google.com"]),
"https://google.com",
|resp| async move {
let resp = resp.unwrap();

assert_eq!(resp.status().as_u16(), StatusCode::INTERNAL_SERVER_ERROR);

let msg = resp.text().await.unwrap();

assert_eq!(
msg.as_str(),
// google.com redirects to www.google.com, but we didn't allow it
"PermissionDenied: Access to www.google.com is not allowed for user worker"
);
},
)
.await;

// 3. deny all hosts
test_allow_net(Some(vec![]), "https://google.com", |resp| async move {
let resp = resp.unwrap();

assert_eq!(resp.status().as_u16(), StatusCode::INTERNAL_SERVER_ERROR);

let msg = resp.text().await.unwrap();

assert_eq!(
msg.as_str(),
"PermissionDenied: Access to google.com is not allowed for user worker"
);
})
.await;

// 4. allow all hosts
test_allow_net(None, "https://google.com", |resp| async move {
let resp = resp.unwrap();

assert_eq!(resp.status().as_u16(), StatusCode::OK);

let payload = resp.json::<FetchResponse>().await.unwrap();

assert_eq!(payload.status, StatusCode::OK);
assert!(!payload.body.is_empty());
})
.await;
}

trait AsyncReadWrite: AsyncRead + AsyncWrite + Send + Unpin {}

impl<T> AsyncReadWrite for T where T: AsyncRead + AsyncWrite + Send + Unpin {}
Expand Down
Loading

0 comments on commit 4a99525

Please sign in to comment.