Support allowlisting of specific capabilities via the SurrealDB CLI (#4763)
This commit is contained in:
parent
2160380fac
commit
0fe9807096
5 changed files with 465 additions and 64 deletions
|
@ -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())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
133
src/dbs/mod.rs
133
src/dbs/mod.rs
|
@ -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(
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue