From 0fe9807096a68a899c8cd84a2b11d6944e725663 Mon Sep 17 00:00:00 2001 From: Gerard Guillemas Martos Date: Mon, 16 Sep 2024 17:36:24 +0200 Subject: [PATCH] Support allowlisting of specific capabilities via the SurrealDB CLI (#4763) --- core/src/ctx/context.rs | 25 +- core/src/fnc/script/tests/fetch.rs | 2 +- core/src/iam/jwks.rs | 2 + src/dbs/mod.rs | 133 +++++++---- tests/cli_integration.rs | 367 ++++++++++++++++++++++++++++- 5 files changed, 465 insertions(+), 64 deletions(-) diff --git a/core/src/ctx/context.rs b/core/src/ctx/context.rs index be269996..9de1a308 100644 --- a/core/src/ctx/context.rs +++ b/core/src/ctx/context.rs @@ -403,32 +403,39 @@ impl MutableContext { #[allow(dead_code)] pub fn check_allowed_scripting(&self) -> Result<(), Error> { if !self.capabilities.allows_scripting() { + warn!("Capabilities denied scripting attempt"); return Err(Error::ScriptingNotAllowed); } + trace!("Capabilities allowed scripting"); Ok(()) } /// Check if a function is allowed pub fn check_allowed_function(&self, target: &str) -> Result<(), Error> { if !self.capabilities.allows_function_name(target) { + warn!("Capabilities denied function execution attempt, target: '{target}'"); return Err(Error::FunctionNotAllowed(target.to_string())); } + trace!("Capabilities allowed function execution, target: '{target}'"); Ok(()) } /// Check if a network target is allowed #[cfg(feature = "http")] - pub fn check_allowed_net(&self, target: &Url) -> Result<(), Error> { - match target.host() { - Some(host) - if self.capabilities.allows_network_target(&NetTarget::Host( - host.to_owned(), - target.port_or_known_default(), - )) => - { + pub fn check_allowed_net(&self, url: &Url) -> Result<(), Error> { + match url.host() { + Some(host) => { + let target = &NetTarget::Host(host.to_owned(), url.port_or_known_default()); + if !self.capabilities.allows_network_target(target) { + warn!( + "Capabilities denied outgoing network connection attempt, target: '{target}'" + ); + return Err(Error::NetTargetNotAllowed(target.to_string())); + } + trace!("Capabilities allowed outgoing network connection, target: '{target}'"); Ok(()) } - _ => Err(Error::NetTargetNotAllowed(target.to_string())), + _ => Err(Error::InvalidUrl(url.to_string())), } } } diff --git a/core/src/fnc/script/tests/fetch.rs b/core/src/fnc/script/tests/fetch.rs index bae661b6..ea1f95e2 100644 --- a/core/src/fnc/script/tests/fetch.rs +++ b/core/src/fnc/script/tests/fetch.rs @@ -196,7 +196,7 @@ async fn test_fetch_denied() { assert!( res.to_string() - .contains(&format!("Access to network target '{}/hello' is not allowed", server.uri())), + .contains(&format!("Access to network target '{}' is not allowed", server.address())), "Unexpected result: {:?}", res ); diff --git a/core/src/iam/jwks.rs b/core/src/iam/jwks.rs index 6e410692..c1a97d4a 100644 --- a/core/src/iam/jwks.rs +++ b/core/src/iam/jwks.rs @@ -279,8 +279,10 @@ fn check_capabilities_url(kvs: &Datastore, url: &str) -> Result<(), Error> { } }; if !kvs.allows_network_target(&target) { + warn!("Capabilities denied outgoing network connection attempt, target: '{target}'"); return Err(Error::InvalidUrl(url.to_string())); } + trace!("Capabilities allowed outgoing network connection, target: '{target}'"); Ok(()) } diff --git a/src/dbs/mod.rs b/src/dbs/mod.rs index 0d216b30..d35dd03c 100644 --- a/src/dbs/mod.rs +++ b/src/dbs/mod.rs @@ -39,44 +39,45 @@ struct DbsCapabilities { // // Allow // - #[arg(help = "Allow all capabilities")] + #[arg(help = "Allow all capabilities except for those more specifically denied")] #[arg(env = "SURREAL_CAPS_ALLOW_ALL", short = 'A', long, conflicts_with = "deny_all")] allow_all: bool, #[cfg(feature = "scripting")] #[arg(help = "Allow execution of embedded scripting functions")] - #[arg(env = "SURREAL_CAPS_ALLOW_SCRIPT", long, conflicts_with = "allow_all")] + #[arg(env = "SURREAL_CAPS_ALLOW_SCRIPT", long, conflicts_with_all = ["allow_all", "deny_scripting"])] allow_scripting: bool, #[arg(help = "Allow guest users to execute queries")] - #[arg(env = "SURREAL_CAPS_ALLOW_GUESTS", long, conflicts_with = "allow_all")] + #[arg(env = "SURREAL_CAPS_ALLOW_GUESTS", long, conflicts_with_all = ["allow_all", "deny_guests"])] allow_guests: bool, #[arg( - help = "Allow execution of all functions. Optionally, you can provide a comma-separated list of function names to allow", - long_help = r#"Allow execution of functions. Optionally, you can provide a comma-separated list of function names to allow. + help = "Allow execution of all functions except for functions that are specifically denied. Alternatively, you can provide a comma-separated list of function names to allow", + long_help = r#"Allow execution of all functions except for functions that are specifically denied. Alternatively, you can provide a comma-separated list of function names to allow +Specifically denied functions and function families prevail over any other allowed function execution. Function names must be in the form [::]. For example: - 'http' or 'http::*' -> Include all functions in the 'http' family - 'http::get' -> Include only the 'get' function in the 'http' family "# )] - #[arg(env = "SURREAL_CAPS_ALLOW_FUNC", long, conflicts_with = "allow_all")] + #[arg(env = "SURREAL_CAPS_ALLOW_FUNC", long)] // If the arg is provided without value, then assume it's "", which gets parsed into Targets::All #[arg(default_missing_value_os = "", num_args = 0..)] - #[arg(default_value_os = "")] // Allow all functions by default #[arg(value_parser = super::cli::validator::func_targets)] allow_funcs: Option>, #[arg( - help = "Allow all outbound network access. Optionally, you can provide a comma-separated list of targets to allow", - long_help = r#"Allow all outbound network access. Optionally, you can provide a comma-separated list of targets to allow. + help = "Allow all outbound network connections except for network targets that are specifically denied. Alternatively, you can provide a comma-separated list of network targets to allow", + long_help = r#"Allow all outbound network connections except for network targets that are specifically denied. Alternatively, you can provide a comma-separated list of network targets to allow +Specifically denied network targets prevail over any other allowed outbound network connections. Targets must be in the form of [:], [/]. For example: - 'surrealdb.com', '127.0.0.1' or 'fd00::1' -> Match outbound connections to these hosts on any port - 'surrealdb.com:80', '127.0.0.1:80' or 'fd00::1:80' -> Match outbound connections to these hosts on port 80 - '10.0.0.0/8' or 'fd00::/8' -> Match outbound connections to any host in these networks "# )] - #[arg(env = "SURREAL_CAPS_ALLOW_NET", long, conflicts_with = "allow_all")] + #[arg(env = "SURREAL_CAPS_ALLOW_NET", long)] // If the arg is provided without value, then assume it's "", which gets parsed into Targets::All #[arg(default_missing_value_os = "", num_args = 0..)] #[arg(value_parser = super::cli::validator::net_targets)] @@ -85,43 +86,45 @@ Targets must be in the form of [:], [/]. For exampl // // Deny // - #[arg(help = "Deny all capabilities")] + #[arg(help = "Deny all capabilities except for those more specifically allowed")] #[arg(env = "SURREAL_CAPS_DENY_ALL", short = 'D', long, conflicts_with = "allow_all")] deny_all: bool, #[cfg(feature = "scripting")] #[arg(help = "Deny execution of embedded scripting functions")] - #[arg(env = "SURREAL_CAPS_DENY_SCRIPT", long, conflicts_with = "deny_all")] + #[arg(env = "SURREAL_CAPS_DENY_SCRIPT", long, conflicts_with_all = ["deny_all", "allow_scripting"])] deny_scripting: bool, #[arg(help = "Deny guest users to execute queries")] - #[arg(env = "SURREAL_CAPS_DENY_GUESTS", long, conflicts_with = "deny_all")] + #[arg(env = "SURREAL_CAPS_DENY_GUESTS", long, conflicts_with_all = ["deny_all", "allow_guests"])] deny_guests: bool, #[arg( - help = "Deny execution of all functions. Optionally, you can provide a comma-separated list of function names to deny", - long_help = r#"Deny execution of functions. Optionally, you can provide a comma-separated list of function names to deny. + help = "Deny execution of all functions except for functions that are specifically allowed. Alternatively, you can provide a comma-separated list of function names to deny", + long_help = r#"Deny execution of all functions except for functions that are specifically allowed. Alternatively, you can provide a comma-separated list of function names to deny. +Specifically allowed functions and function families prevail over a general denial of function execution. Function names must be in the form [::]. For example: - 'http' or 'http::*' -> Include all functions in the 'http' family - 'http::get' -> Include only the 'get' function in the 'http' family "# )] - #[arg(env = "SURREAL_CAPS_DENY_FUNC", long, conflicts_with = "deny_all")] + #[arg(env = "SURREAL_CAPS_DENY_FUNC", long)] // If the arg is provided without value, then assume it's "", which gets parsed into Targets::All #[arg(default_missing_value_os = "", num_args = 0..)] #[arg(value_parser = super::cli::validator::func_targets)] deny_funcs: Option>, #[arg( - help = "Deny all outbound network access. Optionally, you can provide a comma-separated list of targets to deny", - long_help = r#"Deny all outbound network access. Optionally, you can provide a comma-separated list of targets to deny. + help = "Deny all outbound network connections except for network targets that are specifically allowed. Alternatively, you can provide a comma-separated list of network targets to deny", + long_help = r#"Deny all outbound network connections except for network targets that are specifically allowed. Alternatively, you can provide a comma-separated list of network targets to deny. +Specifically allowed network targets prevail over a general denial of outbound network connections. Targets must be in the form of [:], [/]. For example: - 'surrealdb.com', '127.0.0.1' or 'fd00::1' -> Match outbound connections to these hosts on any port - 'surrealdb.com:80', '127.0.0.1:80' or 'fd00::1:80' -> Match outbound connections to these hosts on port 80 - '10.0.0.0/8' or 'fd00::/8' -> Match outbound connections to any host in these networks "# )] - #[arg(env = "SURREAL_CAPS_DENY_NET", long, conflicts_with = "deny_all")] + #[arg(env = "SURREAL_CAPS_DENY_NET", long)] // If the arg is provided without value, then assume it's "", which gets parsed into Targets::All #[arg(default_missing_value_os = "", num_args = 0..)] #[arg(value_parser = super::cli::validator::net_targets)] @@ -131,7 +134,9 @@ Targets must be in the form of [:], [/]. For exampl impl DbsCapabilities { #[cfg(feature = "scripting")] fn get_scripting(&self) -> bool { - (self.allow_all || self.allow_scripting) && !(self.deny_all || self.deny_scripting) + // Even if there was a global deny, we allow if there is a specific allow for scripting + // Even if there is a global allow, we deny if there is a specific deny for scripting + self.allow_scripting || (self.allow_all && !self.deny_scripting) } #[cfg(not(feature = "scripting"))] @@ -140,51 +145,93 @@ impl DbsCapabilities { } fn get_allow_guests(&self) -> bool { - (self.allow_all || self.allow_guests) && !(self.deny_all || self.deny_guests) + // Even if there was a global deny, we allow if there is a specific allow for guests + // Even if there is a global allow, we deny if there is a specific deny for guests + self.allow_guests || (self.allow_all && !self.deny_guests) } fn get_allow_funcs(&self) -> Targets { - if self.deny_all || matches!(self.deny_funcs, Some(Targets::All)) { - return Targets::None; + // If there was a global deny, we allow if there is a general allow or some specific allows for functions + if self.deny_all { + match &self.allow_funcs { + Some(Targets::Some(_)) => return self.allow_funcs.clone().unwrap(), // We already checked for Some + Some(Targets::All) => return Targets::All, + Some(_) => return Targets::None, + None => return Targets::None, + } } + // If there was a general deny for functions, we allow if there are specific allows for functions + if let Some(Targets::All) = self.deny_funcs { + match &self.allow_funcs { + Some(Targets::Some(_)) => return self.allow_funcs.clone().unwrap(), // We already checked for Some + Some(_) => return Targets::None, + None => return Targets::None, + } + } + + // If there are no high level denies but there is a global allow, we allow functions if self.allow_all { return Targets::All; } - // If allow_funcs was not provided and allow_all is false, then don't allow anything (Targets::None) - self.allow_funcs.clone().unwrap_or(Targets::None) + // If there are no high level, we allow the provided functions + // If nothing was provided, we allow functions by default (Targets::All) + self.allow_funcs.clone().unwrap_or(Targets::All) // Functions are enabled by default for the server } fn get_allow_net(&self) -> Targets { - if self.deny_all || matches!(self.deny_net, Some(Targets::All)) { - return Targets::None; + // If there was a global deny, we allow if there is a general allow or some specific allows for networks + if self.deny_all { + match &self.allow_net { + Some(Targets::Some(_)) => return self.allow_net.clone().unwrap(), // We already checked for Some + Some(Targets::All) => return Targets::All, + Some(_) => return Targets::None, + None => return Targets::None, + } } + // If there was a general deny for networks, we allow if there are specific allows for networks + if let Some(Targets::All) = self.deny_net { + match &self.allow_net { + Some(Targets::Some(_)) => return self.allow_net.clone().unwrap(), // We already checked for Some + Some(_) => return Targets::None, + None => return Targets::None, + } + } + + // If there are no high level denies but there is a global allow, we allow networks if self.allow_all { return Targets::All; } - // If allow_net was not provided and allow_all is false, then don't allow anything (Targets::None) + // If there are no high level denies, we allow the provided networks + // If nothing was provided, we do not allow network by default (Targets::None) self.allow_net.clone().unwrap_or(Targets::None) } fn get_deny_funcs(&self) -> Targets { - if self.deny_all { - return Targets::All; + // Allowed functions already consider a global deny and a general deny for functions + // On top of what is explicitly allowed, we deny what is specifically denied + match &self.deny_funcs { + Some(Targets::Some(_)) => self.deny_funcs.clone().unwrap(), // We already checked for Some + Some(_) => Targets::None, + None => Targets::None, } - - // If deny_funcs was not provided and deny_all is false, then don't deny anything (Targets::None) - self.deny_funcs.clone().unwrap_or(Targets::None) } fn get_deny_net(&self) -> Targets { - if self.deny_all { - return Targets::All; + // Allowed networks already consider a global deny and a general deny for networks + // On top of what is explicitly allowed, we deny what is specifically denied + match &self.deny_net { + Some(Targets::Some(_)) => self.deny_net.clone().unwrap(), // We already checked for Some + Some(_) => Targets::None, + None => Targets::None, } + } - // If deny_net was not provided and deny_all is false, then don't deny anything (Targets::None) - self.deny_net.clone().unwrap_or(Targets::None) + fn get_deny_all(&self) -> bool { + self.deny_all } } @@ -212,8 +259,6 @@ pub async fn init( ) -> Result { // Get local copy of options let opt = CF.get().unwrap(); - // Convert the capabilities - let capabilities = capabilities.into(); // Log specified strict mode debug!("Database strict mode is {strict_mode}"); // Log specified query timeout @@ -228,6 +273,12 @@ pub async fn init( if unauthenticated { warn!("❌🔒 IMPORTANT: Authentication is disabled. This is not recommended for production use. 🔒❌"); } + // Warn about the impact of denying all capabilities + if capabilities.get_deny_all() { + warn!("You are denying all capabilities by default. Although this is recommended, beware that any new capabilities will also be denied."); + } + // Convert the capabilities + let capabilities = capabilities.into(); // Log the specified server capabilities debug!("Server capabilities: {capabilities}"); // Parse and setup the desired kv datastore @@ -520,7 +571,7 @@ mod tests { Session::owner(), format!("RETURN http::get('{}')", server1.uri()), false, - format!("Access to network target '{}/' is not allowed", server1.uri()), + format!("Access to network target '{}' is not allowed", server1.address()), ), ( Datastore::new("memory").await.unwrap().with_capabilities( @@ -540,7 +591,7 @@ mod tests { Session::owner(), "RETURN http::get('http://1.1.1.1')".to_string(), false, - "Access to network target 'http://1.1.1.1/' is not allowed".to_string(), + "Access to network target '1.1.1.1:80' is not allowed".to_string(), ), ( Datastore::new("memory").await.unwrap().with_capabilities( diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 6bf1adeb..c4b181a0 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1153,7 +1153,7 @@ mod cli_integration { let query = "RETURN http::get('http://127.0.0.1/');\n\n"; let output = common::run(&cmd).input(query).output().unwrap(); assert!( - output.contains("Access to network target 'http://127.0.0.1/' is not allowed"), + output.contains("Access to network target '127.0.0.1:80' is not allowed"), "unexpected output: {output:?}" ); @@ -1250,10 +1250,259 @@ mod cli_integration { server.finish().unwrap(); } - info!("* When net is denied and function is enabled"); + info!("* When capabilities are denied globally, but functions are allowed generally"); { let (addr, mut server) = common::start_server(StartServerArguments { - args: "--deny-net 127.0.0.1 --allow-funcs http::get".to_owned(), + args: "--deny-all --allow-funcs".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When capabilities are denied globally, but a function family is allowed specifically"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-all --allow-funcs string::len".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When capabilities are allowed globally, but functions are denied generally"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-all --deny-funcs".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + server.finish().unwrap(); + } + + info!("* When capabilities are allowed globally, but a function family is denied specifically"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-all --deny-funcs string::lowercase".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When functions are denied generally, but allowed for a specific family"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-funcs --allow-funcs string::len".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When functions are allowed generally, but denied for a specific family"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-funcs --deny-funcs string::lowercase".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When functions are both allowed and denied specifically but denies are more specific"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-funcs string --deny-funcs string::lowercase".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("[3]"), "unexpected output: {output:?}"); + + server.finish().unwrap(); + } + + info!("* When functions are both allowed and denied specifically but allows are more specific"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-funcs string --allow-funcs string::lowercase".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = "RETURN string::lowercase('SURREALDB');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::lowercase' is not allowed"), + "unexpected output: {output:?}" + ); + + let query = "RETURN string::len('123');\n\n".to_string(); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output.contains("Function 'string::len' is not allowed"), + "unexpected output: {output:?}" + ); + + server.finish().unwrap(); + } + + info!("* When capabilities are denied globally, but net is allowed generally"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-all --allow-net --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("['surrealdb-"), "unexpected output: {output:?}"); + server.finish().unwrap(); + } + + info!("* When capabilities are denied globally, but net is allowed specifically"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-all --allow-net 127.0.0.1 --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("['surrealdb-"), "unexpected output: {output:?}"); + server.finish().unwrap(); + } + + info!("* When capabilities are allowed globally, but net is denied generally"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-all --deny-net --allow-funcs http::get".to_owned(), ..Default::default() }) .await @@ -1267,16 +1516,82 @@ mod cli_integration { let query = format!("RETURN http::get('http://{addr}/version');\n\n"); let output = common::run(&cmd).input(&query).output().unwrap(); assert!( - output.contains( - format!("Access to network target 'http://{addr}/version' is not allowed") - .as_str() - ), + output + .contains(format!("Access to network target '{addr}' is not allowed").as_str()), "unexpected output: {output:?}" ); server.finish().unwrap(); } - info!("* When net is enabled for an IP and also denied for a specific port that doesn't match"); + info!("* When capabilities are allowed globally, but net is denied specifically"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-all --deny-net 127.0.0.1 --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output + .contains(format!("Access to network target '{addr}' is not allowed").as_str()), + "unexpected output: {output:?}" + ); + server.finish().unwrap(); + } + + info!("* When net is denied generally, but allowed for a specific address"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-net --allow-net 127.0.0.1 --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!(output.contains("['surrealdb-"), "unexpected output: {output:?}"); + server.finish().unwrap(); + } + + info!("* When net is allowed generally, but denied for a specific address"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--allow-net --deny-net 127.0.0.1 --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output + .contains(format!("Access to network target '{addr}' is not allowed").as_str()), + "unexpected output: {output:?}" + ); + server.finish().unwrap(); + } + + info!("* When net is both allowed and denied specifically but denies are more specific"); { let (addr, mut server) = common::start_server(StartServerArguments { args: "--allow-net 127.0.0.1 --deny-net 127.0.0.1:80 --allow-funcs http::get" @@ -1297,10 +1612,11 @@ mod cli_integration { server.finish().unwrap(); } - info!("* When a function family is denied"); + info!("* When net is both allowed and denied specifically but allows are more specific"); { let (addr, mut server) = common::start_server(StartServerArguments { - args: "--deny-funcs http".to_owned(), + args: "--deny-net 127.0.0.1 --allow-net 127.0.0.1:80 --allow-funcs http::get" + .to_owned(), ..Default::default() }) .await @@ -1311,10 +1627,35 @@ mod cli_integration { throwaway = Ulid::new() ); - let query = "RETURN http::get('https://surrealdb.com');\n\n"; - let output = common::run(&cmd).input(query).output().unwrap(); + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); assert!( - output.contains("Function 'http::get' is not allowed"), + output + .contains(format!("Access to network target '{addr}' is not allowed").as_str()), + "unexpected output: {output:?}" + ); + server.finish().unwrap(); + } + + info!("* When net is denied specifically and functions are enabled specifically"); + { + let (addr, mut server) = common::start_server(StartServerArguments { + args: "--deny-net 127.0.0.1 --allow-funcs http::get".to_owned(), + ..Default::default() + }) + .await + .unwrap(); + + let cmd = format!( + "sql --conn ws://{addr} -u root -p root --ns {throwaway} --db {throwaway} --multi", + throwaway = Ulid::new() + ); + + let query = format!("RETURN http::get('http://{addr}/version');\n\n"); + let output = common::run(&cmd).input(&query).output().unwrap(); + assert!( + output + .contains(format!("Access to network target '{addr}' is not allowed").as_str()), "unexpected output: {output:?}" ); server.finish().unwrap();