Allow authenticating with a token from the CLI (#4362)
This commit is contained in:
parent
a81aef0e93
commit
84d289d868
9 changed files with 375 additions and 0 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5873,6 +5873,7 @@ dependencies = [
|
|||
"axum-server",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
|
@ -5905,6 +5906,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serial_test",
|
||||
"surrealdb",
|
||||
"surrealdb-jsonwebtoken",
|
||||
"temp-env",
|
||||
"tempfile",
|
||||
"test-log",
|
||||
|
|
|
@ -132,7 +132,9 @@ jemallocator = "0.5.4"
|
|||
|
||||
[dev-dependencies]
|
||||
assert_fs = "1.0.13"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
env_logger = "0.10.1"
|
||||
jsonwebtoken = { version = "8.3.0-surreal.1", package = "surrealdb-jsonwebtoken" }
|
||||
opentelemetry-proto = { version = "0.2.0", features = [
|
||||
"gen-tonic",
|
||||
"traces",
|
||||
|
|
|
@ -23,6 +23,14 @@ pub(crate) struct AuthArguments {
|
|||
requires = "username"
|
||||
)]
|
||||
pub(crate) password: Option<String>,
|
||||
#[arg(help = "Authentication token in JWT format to use when connecting")]
|
||||
#[arg(
|
||||
env = "SURREAL_TOKEN",
|
||||
short = 't',
|
||||
long = "token",
|
||||
conflicts_with_all = ["username", "password", "auth_level"],
|
||||
)]
|
||||
pub(crate) token: Option<String>,
|
||||
#[arg(help = "Authentication level to use when connecting")]
|
||||
#[arg(env = "SURREAL_AUTH_LEVEL", long = "auth-level", default_value = "root")]
|
||||
#[arg(value_parser = super::validator::parser::creds_level::CredentialsLevelParser::new())]
|
||||
|
|
|
@ -32,6 +32,7 @@ pub async fn init(
|
|||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
auth_level,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
|
@ -65,6 +66,11 @@ pub async fn init(
|
|||
CredentialsLevel::Database => client.signin(creds.database()?).await?,
|
||||
};
|
||||
|
||||
client
|
||||
} else if token.is_some() && !endpoint.clone().into_endpoint()?.parse_kind()?.is_local() {
|
||||
let client = connect(endpoint).await?;
|
||||
client.authenticate(token.unwrap()).await?;
|
||||
|
||||
client
|
||||
} else {
|
||||
debug!("Connecting to the database engine without authentication");
|
||||
|
|
|
@ -29,6 +29,7 @@ pub async fn init(
|
|||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
auth_level,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
|
@ -64,6 +65,11 @@ pub async fn init(
|
|||
CredentialsLevel::Database => client.signin(creds.database()?).await?,
|
||||
};
|
||||
|
||||
client
|
||||
} else if token.is_some() && !endpoint.clone().into_endpoint()?.parse_kind()?.is_local() {
|
||||
let client = connect(endpoint).await?;
|
||||
client.authenticate(token.unwrap()).await?;
|
||||
|
||||
client
|
||||
} else {
|
||||
debug!("Connecting to the database engine without authentication");
|
||||
|
|
|
@ -48,6 +48,7 @@ pub async fn init(
|
|||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
auth_level,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
|
@ -81,6 +82,11 @@ pub async fn init(
|
|||
CredentialsLevel::Database => client.signin(creds.database()?).await?,
|
||||
};
|
||||
|
||||
client
|
||||
} else if token.is_some() && !endpoint.clone().into_endpoint()?.parse_kind()?.is_local() {
|
||||
let client = connect(endpoint).await?;
|
||||
client.authenticate(token.unwrap()).await?;
|
||||
|
||||
client
|
||||
} else {
|
||||
debug!("Connecting to the database engine without authentication");
|
||||
|
|
|
@ -29,6 +29,7 @@ pub async fn init(
|
|||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
auth_level,
|
||||
},
|
||||
sel: DatabaseSelectionArguments {
|
||||
|
@ -64,6 +65,11 @@ pub async fn init(
|
|||
CredentialsLevel::Database => client.signin(creds.database()?).await?,
|
||||
};
|
||||
|
||||
client
|
||||
} else if token.is_some() && !endpoint.clone().into_endpoint()?.parse_kind()?.is_local() {
|
||||
let client = connect(endpoint).await?;
|
||||
client.authenticate(token.unwrap()).await?;
|
||||
|
||||
client
|
||||
} else {
|
||||
debug!("Connecting to the database engine without authentication");
|
||||
|
|
|
@ -44,6 +44,7 @@ pub async fn init(
|
|||
auth: AuthArguments {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
auth_level,
|
||||
},
|
||||
conn: DatabaseConnectionArguments {
|
||||
|
@ -87,6 +88,11 @@ pub async fn init(
|
|||
CredentialsLevel::Database => client.signin(creds.database()?).await?,
|
||||
};
|
||||
|
||||
client
|
||||
} else if token.is_some() && !endpoint.clone().into_endpoint()?.parse_kind()?.is_local() {
|
||||
let client = connect(endpoint).await?;
|
||||
client.authenticate(token.unwrap()).await?;
|
||||
|
||||
client
|
||||
} else {
|
||||
debug!("Connecting to the database engine without authentication");
|
||||
|
|
|
@ -4,8 +4,12 @@ mod common;
|
|||
mod cli_integration {
|
||||
use crate::remove_debug_info;
|
||||
use assert_fs::prelude::{FileTouch, FileWriteStr, PathChild};
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::Utc;
|
||||
use common::Format;
|
||||
use common::Socket;
|
||||
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fs::File;
|
||||
use std::time;
|
||||
|
@ -488,6 +492,335 @@ mod cli_integration {
|
|||
server.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn with_auth_level_token() {
|
||||
// Commands with token for different auth levels
|
||||
let (addr, mut server) = common::start_server_with_defaults().await.unwrap();
|
||||
let ac = "test";
|
||||
let ns = Ulid::new();
|
||||
let db = Ulid::new();
|
||||
let exp = (Utc::now() + ChronoDuration::days(1)).timestamp();
|
||||
let key = "secret";
|
||||
let header = Header::new(Algorithm::HS512);
|
||||
let key_enc = EncodingKey::from_secret(key.as_ref());
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
exp: Option<i64>,
|
||||
iss: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ns: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
db: Option<String>,
|
||||
ac: Option<String>,
|
||||
rl: Option<Vec<String>>,
|
||||
}
|
||||
let claims_root = serde_json::from_str::<Claims>(
|
||||
format!(
|
||||
r#"
|
||||
{{
|
||||
"iss": "surrealdb-test",
|
||||
"exp": {exp},
|
||||
"ac": "{ac}",
|
||||
"rl": ["Owner"]
|
||||
}}
|
||||
"#
|
||||
)
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
let token_root = match encode(&header, &claims_root, &key_enc) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
let claims_ns = serde_json::from_str::<Claims>(
|
||||
format!(
|
||||
r#"
|
||||
{{
|
||||
"iss": "surrealdb-test",
|
||||
"exp": {exp},
|
||||
"ac": "{ac}",
|
||||
"ns": "{ns}",
|
||||
"rl": ["Owner"]
|
||||
}}
|
||||
"#
|
||||
)
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
let token_ns = match encode(&header, &claims_ns, &key_enc) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
let claims_db = serde_json::from_str::<Claims>(
|
||||
format!(
|
||||
r#"
|
||||
{{
|
||||
"iss": "surrealdb-test",
|
||||
"exp": {exp},
|
||||
"ac": "{ac}",
|
||||
"ns": "{ns}",
|
||||
"db": "{db}",
|
||||
"rl": ["Owner"]
|
||||
}}
|
||||
"#
|
||||
)
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
let token_db = match encode(&header, &claims_db, &key_enc) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
let record_user = "user:1";
|
||||
let claims_record = serde_json::from_str::<Claims>(
|
||||
format!(
|
||||
r#"
|
||||
{{
|
||||
"iss": "surrealdb-test",
|
||||
"exp": {exp},
|
||||
"ac": "{ac}",
|
||||
"ns": "{ns}",
|
||||
"db": "{db}",
|
||||
"id": "{record_user}"
|
||||
}}
|
||||
"#
|
||||
)
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
let token_record = match encode(&header, &claims_record, &key_enc) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
|
||||
info!("* Create access methods with identical names at ROOT, NS and DB levels");
|
||||
{
|
||||
let args =
|
||||
format!("sql --conn http://{addr} --db {db} --ns {ns} --user {USER} --pass {PASS}");
|
||||
let _ = common::run(&args)
|
||||
.input(format!("DEFINE ACCESS {ac} ON ROOT TYPE JWT ALGORITHM HS512 KEY '{key}';
|
||||
DEFINE ACCESS {ac} ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '{key}';
|
||||
DEFINE ACCESS {ac} ON DATABASE TYPE JWT ALGORITHM HS512 KEY '{key}';\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
}
|
||||
|
||||
info!("* Create record that will be used as record user for authentication");
|
||||
{
|
||||
let args =
|
||||
format!("sql --conn http://{addr} --db {db} --ns {ns} --user {USER} --pass {PASS}");
|
||||
let _ = common::run(&args)
|
||||
.input(format!("CREATE {record_user};").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
}
|
||||
|
||||
info!("* Pass root token and access root info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_root}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR ROOT;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("namespaces: {"),
|
||||
"root token should be able to access root info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass root token and access namespace info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_root}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR NS;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("databases: {"),
|
||||
"root token should be able to access namespace info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass root auth level and access database info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_root}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"root token should be able to access database info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass namespace token and access root info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_ns}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR ROOT;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("IAM error: Not enough permissions to perform this action"),
|
||||
"namespace token should not be able to access root info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass namespace token and access namespace info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_ns}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR NS;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("databases: {"),
|
||||
"namespace token should be able to access namespace info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass namespace token and access database info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_ns}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"namespace token should be able to access database info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass database token and access root info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_db}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR ROOT;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("IAM error: Not enough permissions to perform this action"),
|
||||
"database token should not be able to access root info: {output}",
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass database token and access namespace info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_db}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR NS;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("IAM error: Not enough permissions to perform this action"),
|
||||
"database token should not be able to access namespace info: {output}",
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass database token and access database info");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_db}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"database token should be able to access database info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass database token without specifying namespace or database");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --token {token_db}");
|
||||
let output = common::run(&args).input("INFO FOR DB;\n").output().expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"namespace and database to use should be selected from token: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass record user token and access database info");
|
||||
{
|
||||
let args =
|
||||
format!("sql --conn http://{addr} --db {db} --ns {ns} --token {token_record}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"database token should be able to access database info: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass namespace token without specifying namespace");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --token {token_ns}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR NS;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("databases: {"),
|
||||
"namespace should be selected from token: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass database token without specifying database");
|
||||
{
|
||||
let args = format!("sql --conn http://{addr} --ns {ns} --token {token_db}");
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output()
|
||||
.expect("success");
|
||||
assert!(
|
||||
output.contains("tables: {"),
|
||||
"database should be selected from token: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass token at the same time as credentials");
|
||||
{
|
||||
let args = format!(
|
||||
"sql --conn http://{addr} --ns {ns} --token {token_db} -u {USER} -p {PASS}"
|
||||
);
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output();
|
||||
assert!(
|
||||
output
|
||||
.clone()
|
||||
.unwrap_err()
|
||||
.contains("error: the argument '--token <TOKEN>' cannot be used with"),
|
||||
"the token argument should not be compatible with credentials: {:?}",
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
info!("* Pass token at the same time as different auth level");
|
||||
{
|
||||
let args = format!(
|
||||
"sql --conn http://{addr} --ns {ns} --token {token_db} --auth-level namespace"
|
||||
);
|
||||
let output = common::run(&args)
|
||||
.input(format!("USE NS `{ns}` DB `{db}`; INFO FOR DB;\n").as_str())
|
||||
.output();
|
||||
assert!(
|
||||
output
|
||||
.clone()
|
||||
.unwrap_err()
|
||||
.contains("error: the argument '--token <TOKEN>' cannot be used with"),
|
||||
"the token argument should not be compatible with authentication level: {:?}",
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
server.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn with_anon_auth() {
|
||||
// Commands without credentials when auth is enabled, should fail
|
||||
|
|
Loading…
Reference in a new issue