Support allowlisting of specific capabilities via the SurrealDB CLI (#4763)

This commit is contained in:
Gerard Guillemas Martos 2024-09-16 17:36:24 +02:00 committed by GitHub
parent 2160380fac
commit 0fe9807096
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 465 additions and 64 deletions

View file

@ -403,32 +403,39 @@ impl MutableContext {
#[allow(dead_code)] #[allow(dead_code)]
pub fn check_allowed_scripting(&self) -> Result<(), Error> { pub fn check_allowed_scripting(&self) -> Result<(), Error> {
if !self.capabilities.allows_scripting() { if !self.capabilities.allows_scripting() {
warn!("Capabilities denied scripting attempt");
return Err(Error::ScriptingNotAllowed); return Err(Error::ScriptingNotAllowed);
} }
trace!("Capabilities allowed scripting");
Ok(()) Ok(())
} }
/// Check if a function is allowed /// Check if a function is allowed
pub fn check_allowed_function(&self, target: &str) -> Result<(), Error> { pub fn check_allowed_function(&self, target: &str) -> Result<(), Error> {
if !self.capabilities.allows_function_name(target) { if !self.capabilities.allows_function_name(target) {
warn!("Capabilities denied function execution attempt, target: '{target}'");
return Err(Error::FunctionNotAllowed(target.to_string())); return Err(Error::FunctionNotAllowed(target.to_string()));
} }
trace!("Capabilities allowed function execution, target: '{target}'");
Ok(()) Ok(())
} }
/// Check if a network target is allowed /// Check if a network target is allowed
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub fn check_allowed_net(&self, target: &Url) -> Result<(), Error> { pub fn check_allowed_net(&self, url: &Url) -> Result<(), Error> {
match target.host() { match url.host() {
Some(host) Some(host) => {
if self.capabilities.allows_network_target(&NetTarget::Host( let target = &NetTarget::Host(host.to_owned(), url.port_or_known_default());
host.to_owned(), if !self.capabilities.allows_network_target(target) {
target.port_or_known_default(), 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(()) Ok(())
} }
_ => Err(Error::NetTargetNotAllowed(target.to_string())), _ => Err(Error::InvalidUrl(url.to_string())),
} }
} }
} }

View file

@ -196,7 +196,7 @@ async fn test_fetch_denied() {
assert!( assert!(
res.to_string() 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: {:?}", "Unexpected result: {:?}",
res res
); );

View file

@ -279,8 +279,10 @@ fn check_capabilities_url(kvs: &Datastore, url: &str) -> Result<(), Error> {
} }
}; };
if !kvs.allows_network_target(&target) { if !kvs.allows_network_target(&target) {
warn!("Capabilities denied outgoing network connection attempt, target: '{target}'");
return Err(Error::InvalidUrl(url.to_string())); return Err(Error::InvalidUrl(url.to_string()));
} }
trace!("Capabilities allowed outgoing network connection, target: '{target}'");
Ok(()) Ok(())
} }

View file

@ -39,44 +39,45 @@ struct DbsCapabilities {
// //
// Allow // 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")] #[arg(env = "SURREAL_CAPS_ALLOW_ALL", short = 'A', long, conflicts_with = "deny_all")]
allow_all: bool, allow_all: bool,
#[cfg(feature = "scripting")] #[cfg(feature = "scripting")]
#[arg(help = "Allow execution of embedded scripting functions")] #[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, allow_scripting: bool,
#[arg(help = "Allow guest users to execute queries")] #[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, allow_guests: bool,
#[arg( #[arg(
help = "Allow execution of all 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 functions. Optionally, 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 <family>[::<name>]. For example: Function names must be in the form <family>[::<name>]. For example:
- 'http' or 'http::*' -> Include all functions in the 'http' family - 'http' or 'http::*' -> Include all functions in the 'http' family
- 'http::get' -> Include only the 'get' function 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 // 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_missing_value_os = "", num_args = 0..)]
#[arg(default_value_os = "")] // Allow all functions by default
#[arg(value_parser = super::cli::validator::func_targets)] #[arg(value_parser = super::cli::validator::func_targets)]
allow_funcs: Option<Targets<FuncTarget>>, allow_funcs: Option<Targets<FuncTarget>>,
#[arg( #[arg(
help = "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 access. Optionally, you can provide a comma-separated list of 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 <host>[:<port>], <ipv4|ipv6>[/<mask>]. For example: Targets must be in the form of <host>[:<port>], <ipv4|ipv6>[/<mask>]. For example:
- 'surrealdb.com', '127.0.0.1' or 'fd00::1' -> Match outbound connections to these hosts on any port - '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 - '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 - '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 // 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_missing_value_os = "", num_args = 0..)]
#[arg(value_parser = super::cli::validator::net_targets)] #[arg(value_parser = super::cli::validator::net_targets)]
@ -85,43 +86,45 @@ Targets must be in the form of <host>[:<port>], <ipv4|ipv6>[/<mask>]. For exampl
// //
// Deny // 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")] #[arg(env = "SURREAL_CAPS_DENY_ALL", short = 'D', long, conflicts_with = "allow_all")]
deny_all: bool, deny_all: bool,
#[cfg(feature = "scripting")] #[cfg(feature = "scripting")]
#[arg(help = "Deny execution of embedded scripting functions")] #[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, deny_scripting: bool,
#[arg(help = "Deny guest users to execute queries")] #[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, deny_guests: bool,
#[arg( #[arg(
help = "Deny execution of all 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 functions. Optionally, 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 <family>[::<name>]. For example: Function names must be in the form <family>[::<name>]. For example:
- 'http' or 'http::*' -> Include all functions in the 'http' family - 'http' or 'http::*' -> Include all functions in the 'http' family
- 'http::get' -> Include only the 'get' function 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 // 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_missing_value_os = "", num_args = 0..)]
#[arg(value_parser = super::cli::validator::func_targets)] #[arg(value_parser = super::cli::validator::func_targets)]
deny_funcs: Option<Targets<FuncTarget>>, deny_funcs: Option<Targets<FuncTarget>>,
#[arg( #[arg(
help = "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 access. Optionally, you can provide a comma-separated list of 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 <host>[:<port>], <ipv4|ipv6>[/<mask>]. For example: Targets must be in the form of <host>[:<port>], <ipv4|ipv6>[/<mask>]. For example:
- 'surrealdb.com', '127.0.0.1' or 'fd00::1' -> Match outbound connections to these hosts on any port - '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 - '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 - '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 // 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_missing_value_os = "", num_args = 0..)]
#[arg(value_parser = super::cli::validator::net_targets)] #[arg(value_parser = super::cli::validator::net_targets)]
@ -131,7 +134,9 @@ Targets must be in the form of <host>[:<port>], <ipv4|ipv6>[/<mask>]. For exampl
impl DbsCapabilities { impl DbsCapabilities {
#[cfg(feature = "scripting")] #[cfg(feature = "scripting")]
fn get_scripting(&self) -> bool { 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"))] #[cfg(not(feature = "scripting"))]
@ -140,51 +145,93 @@ impl DbsCapabilities {
} }
fn get_allow_guests(&self) -> bool { 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<FuncTarget> { fn get_allow_funcs(&self) -> Targets<FuncTarget> {
if self.deny_all || matches!(self.deny_funcs, Some(Targets::All)) { // If there was a global deny, we allow if there is a general allow or some specific allows for functions
return Targets::None; 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 { if self.allow_all {
return Targets::All; return Targets::All;
} }
// If allow_funcs was not provided and allow_all is false, then don't allow anything (Targets::None) // If there are no high level, we allow the provided functions
self.allow_funcs.clone().unwrap_or(Targets::None) // 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<NetTarget> { fn get_allow_net(&self) -> Targets<NetTarget> {
if self.deny_all || matches!(self.deny_net, Some(Targets::All)) { // If there was a global deny, we allow if there is a general allow or some specific allows for networks
return Targets::None; 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 { if self.allow_all {
return Targets::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) self.allow_net.clone().unwrap_or(Targets::None)
} }
fn get_deny_funcs(&self) -> Targets<FuncTarget> { fn get_deny_funcs(&self) -> Targets<FuncTarget> {
if self.deny_all { // Allowed functions already consider a global deny and a general deny for functions
return Targets::All; // 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<NetTarget> { fn get_deny_net(&self) -> Targets<NetTarget> {
if self.deny_all { // Allowed networks already consider a global deny and a general deny for networks
return Targets::All; // 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) fn get_deny_all(&self) -> bool {
self.deny_net.clone().unwrap_or(Targets::None) self.deny_all
} }
} }
@ -212,8 +259,6 @@ pub async fn init(
) -> Result<Datastore, Error> { ) -> Result<Datastore, Error> {
// Get local copy of options // Get local copy of options
let opt = CF.get().unwrap(); let opt = CF.get().unwrap();
// Convert the capabilities
let capabilities = capabilities.into();
// Log specified strict mode // Log specified strict mode
debug!("Database strict mode is {strict_mode}"); debug!("Database strict mode is {strict_mode}");
// Log specified query timeout // Log specified query timeout
@ -228,6 +273,12 @@ pub async fn init(
if unauthenticated { if unauthenticated {
warn!("❌🔒 IMPORTANT: Authentication is disabled. This is not recommended for production use. 🔒❌"); 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 // Log the specified server capabilities
debug!("Server capabilities: {capabilities}"); debug!("Server capabilities: {capabilities}");
// Parse and setup the desired kv datastore // Parse and setup the desired kv datastore
@ -520,7 +571,7 @@ mod tests {
Session::owner(), Session::owner(),
format!("RETURN http::get('{}')", server1.uri()), format!("RETURN http::get('{}')", server1.uri()),
false, 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( Datastore::new("memory").await.unwrap().with_capabilities(
@ -540,7 +591,7 @@ mod tests {
Session::owner(), Session::owner(),
"RETURN http::get('http://1.1.1.1')".to_string(), "RETURN http::get('http://1.1.1.1')".to_string(),
false, 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( Datastore::new("memory").await.unwrap().with_capabilities(

View file

@ -1153,7 +1153,7 @@ mod cli_integration {
let query = "RETURN http::get('http://127.0.0.1/');\n\n"; let query = "RETURN http::get('http://127.0.0.1/');\n\n";
let output = common::run(&cmd).input(query).output().unwrap(); let output = common::run(&cmd).input(query).output().unwrap();
assert!( 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:?}" "unexpected output: {output:?}"
); );
@ -1250,10 +1250,259 @@ mod cli_integration {
server.finish().unwrap(); 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 { 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() ..Default::default()
}) })
.await .await
@ -1267,16 +1516,82 @@ mod cli_integration {
let query = format!("RETURN http::get('http://{addr}/version');\n\n"); let query = format!("RETURN http::get('http://{addr}/version');\n\n");
let output = common::run(&cmd).input(&query).output().unwrap(); let output = common::run(&cmd).input(&query).output().unwrap();
assert!( assert!(
output.contains( output
format!("Access to network target 'http://{addr}/version' is not allowed") .contains(format!("Access to network target '{addr}' is not allowed").as_str()),
.as_str()
),
"unexpected output: {output:?}" "unexpected output: {output:?}"
); );
server.finish().unwrap(); 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 { 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" 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(); 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 { 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() ..Default::default()
}) })
.await .await
@ -1311,10 +1627,35 @@ mod cli_integration {
throwaway = Ulid::new() throwaway = Ulid::new()
); );
let query = "RETURN http::get('https://surrealdb.com');\n\n"; let query = format!("RETURN http::get('http://{addr}/version');\n\n");
let output = common::run(&cmd).input(query).output().unwrap(); let output = common::run(&cmd).input(&query).output().unwrap();
assert!( 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:?}" "unexpected output: {output:?}"
); );
server.finish().unwrap(); server.finish().unwrap();