Revert to using legacy authentication in signin by default (#3052)

Co-authored-by: Salvador Girones Gil <salvadorgirones@gmail.com>
This commit is contained in:
Gerard Guillemas Martos 2023-12-04 09:47:37 +01:00 committed by GitHub
parent 69572e9e6b
commit bc4ffcb4cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 472 additions and 35 deletions

View file

@ -1,4 +1,4 @@
use super::verify::{verify_db_creds, verify_ns_creds, verify_root_creds};
use super::verify::{verify_creds_legacy, verify_db_creds, verify_ns_creds, verify_root_creds};
use super::{Actor, Level};
use crate::cnf::{INSECURE_FORWARD_SCOPE_ERRORS, SERVER_NAME};
use crate::dbs::Session;
@ -194,7 +194,16 @@ pub async fn db(
user: String,
pass: String,
) -> Result<Option<String>, Error> {
match verify_db_creds(kvs, &ns, &db, &user, &pass).await {
let verify_creds = if kvs.is_auth_level_enabled() {
verify_db_creds(kvs, &ns, &db, &user, &pass).await
} else {
// TODO(gguillemas): Remove this condition once the legacy authentication is deprecated in v2.0.0
match verify_creds_legacy(kvs, Some(&ns), Some(&db), &user, &pass).await {
Ok((_, u)) => Ok(u),
Err(e) => Err(e),
}
};
match verify_creds {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
@ -236,7 +245,16 @@ pub async fn ns(
user: String,
pass: String,
) -> Result<Option<String>, Error> {
match verify_ns_creds(kvs, &ns, &user, &pass).await {
let verify_creds = if kvs.is_auth_level_enabled() {
verify_ns_creds(kvs, &ns, &user, &pass).await
} else {
// TODO(gguillemas): Remove this condition once the legacy authentication is deprecated in v2.0.0
match verify_creds_legacy(kvs, Some(&ns), None, &user, &pass).await {
Ok((_, u)) => Ok(u),
Err(e) => Err(e),
}
};
match verify_creds {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
@ -276,7 +294,16 @@ pub async fn root(
user: String,
pass: String,
) -> Result<Option<String>, Error> {
match verify_root_creds(kvs, &user, &pass).await {
let verify_creds = if kvs.is_auth_level_enabled() {
verify_root_creds(kvs, &user, &pass).await
} else {
// TODO(gguillemas): Remove this condition once the legacy authentication is deprecated in v2.0.0
match verify_creds_legacy(kvs, None, None, &user, &pass).await {
Ok((_, u)) => Ok(u),
Err(e) => Err(e),
}
};
match verify_creds {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());

View file

@ -125,7 +125,7 @@ pub async fn basic(
}
}
// TODO(gguillemas): Remove this method once the legacy basic auth is deprecated in v2.0.0
// TODO(gguillemas): Remove this method once the legacy authentication is deprecated in v2.0.0
pub async fn basic_legacy(
kvs: &Datastore,
session: &mut Session,
@ -501,7 +501,7 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
}
}
// TODO(gguillemas): Remove this method once the legacy basic auth is deprecated in v2.0.0
// TODO(gguillemas): Remove this method once the legacy authentication is deprecated in v2.0.0
pub async fn verify_creds_legacy(
ds: &Datastore,
ns: Option<&String>,

View file

@ -92,6 +92,9 @@ pub struct Datastore {
strict: bool,
// Whether authentication is enabled on this datastore.
auth_enabled: bool,
// Whether authentication level is enabled on this datastore.
// TODO(gguillemas): Remove this field once the legacy authentication is deprecated in v2.0.0
auth_level_enabled: bool,
// The maximum duration timeout for running multiple statements in a query
query_timeout: Option<Duration>,
// The maximum duration timeout for running multiple statements in a transaction
@ -340,6 +343,8 @@ impl Datastore {
inner,
strict: false,
auth_enabled: false,
// TODO(gguillemas): Remove this field once the legacy authentication is deprecated in v2.0.0
auth_level_enabled: false,
query_timeout: None,
transaction_timeout: None,
notification_channel: None,
@ -385,6 +390,13 @@ impl Datastore {
self
}
/// Set whether authentication levels are enabled for this Datastore
/// TODO(gguillemas): Remove this method once the legacy authentication is deprecated in v2.0.0
pub fn with_auth_level_enabled(mut self, enabled: bool) -> Self {
self.auth_level_enabled = enabled;
self
}
/// Set specific capabilities for this Datastore
pub fn with_capabilities(mut self, caps: Capabilities) -> Self {
self.capabilities = caps;
@ -396,6 +408,12 @@ impl Datastore {
self.auth_enabled
}
/// Is authentication level enabled for this Datastore?
/// TODO(gguillemas): Remove this method once the legacy authentication is deprecated in v2.0.0
pub fn is_auth_level_enabled(&self) -> bool {
self.auth_level_enabled
}
/// Setup the initial credentials
/// Trigger the `unreachable definition` compilation error, probably due to this issue:
/// https://github.com/rust-lang/rust/issues/111370

View file

@ -23,7 +23,7 @@ pub(crate) struct AuthArguments {
requires = "username"
)]
pub(crate) password: Option<String>,
// TODO(gguillemas): Update this help message once the legacy basic auth is deprecated in v2.0.0
// TODO(gguillemas): Update this help message once the legacy authentication is deprecated in v2.0.0
// Explicit level authentication will be enabled by default after the deprecation
#[arg(
help = "Authentication level to use when connecting\nMust be enabled in the server and uses the values of '--namespace' and '--database'\n"

View file

@ -17,7 +17,6 @@ pub struct Config {
pub client_ip: ClientIp,
pub user: Option<String>,
pub pass: Option<String>,
pub enable_auth_level: bool,
pub crt: Option<PathBuf>,
pub key: Option<PathBuf>,
pub tick_interval: Duration,

View file

@ -74,14 +74,6 @@ pub struct StartCommandArguments {
requires = "username"
)]
password: Option<String>,
// TODO(gguillemas): Remove this arg once the legacy basic auth is deprecated in v2.0.0
// Explicit level authentication will be enabled by default after the deprecation
#[arg(
help = "Support specifying the level at which to authenticate",
help_heading = "Authentication"
)]
#[arg(env = "SURREAL_ENABLE_AUTH_LEVEL", long = "enable-auth-level")]
pub(crate) enable_auth_level: bool,
//
// Datastore connection
@ -143,7 +135,6 @@ pub async fn init(
path,
username: user,
password: pass,
enable_auth_level,
client_ip,
listen_addresses,
dbs,
@ -179,7 +170,6 @@ pub async fn init(
path,
user,
pass,
enable_auth_level,
tick_interval,
crt: web.as_ref().and_then(|x| x.web_crt.clone()),
key: web.as_ref().and_then(|x| x.web_key.clone()),

View file

@ -27,6 +27,14 @@ pub struct StartCommandDbsOptions {
#[arg(env = "SURREAL_AUTH", long = "auth")]
#[arg(default_value_t = false)]
auth_enabled: bool,
// TODO(gguillemas): Remove this argument once the legacy authentication is deprecated in v2.0.0
#[arg(
help = "Whether to enable explicit authentication level selection",
help_heading = "Authentication"
)]
#[arg(env = "SURREAL_AUTH_LEVEL_ENABLED", long = "auth-level-enabled")]
#[arg(default_value_t = false)]
auth_level_enabled: bool,
#[command(flatten)]
#[command(next_help_heading = "Capabilities")]
caps: DbsCapabilities,
@ -204,6 +212,8 @@ pub async fn init(
query_timeout,
transaction_timeout,
auth_enabled,
// TODO(gguillemas): Remove this field once the legacy authentication is deprecated in v2.0.0
auth_level_enabled,
caps,
}: StartCommandDbsOptions,
) -> Result<(), Error> {
@ -225,6 +235,11 @@ pub async fn init(
} else {
warn!("❌🔒 IMPORTANT: Authentication is disabled. This is not recommended for production use. 🔒❌");
}
// Log whether authentication levels are enabled
// TODO(gguillemas): Remove this condition once the legacy authentication is deprecated in v2.0.0
if auth_level_enabled {
info!("Authentication levels are enabled");
}
let caps = caps.into();
debug!("Server capabilities: {caps}");
@ -237,6 +252,7 @@ pub async fn init(
.with_query_timeout(query_timeout)
.with_transaction_timeout(transaction_timeout)
.with_auth_enabled(auth_enabled)
.with_auth_level_enabled(auth_level_enabled)
.with_capabilities(caps);
dbs.bootstrap().await?;

View file

@ -15,7 +15,6 @@ use surrealdb::{
};
use tower_http::auth::AsyncAuthorizeRequest;
use crate::cli::CF;
use crate::{dbs::DB, err::Error};
use super::{
@ -140,10 +139,7 @@ async fn check_auth(parts: &mut Parts) -> Result<Session, Error> {
// If Basic authentication data was supplied
if let Ok(au) = parts.extract::<TypedHeader<Authorization<Basic>>>().await {
// Get local copy of options
let opt = CF.get().unwrap();
if opt.enable_auth_level {
if kvs.is_auth_level_enabled() {
basic(
kvs,
&mut session,

View file

@ -386,7 +386,7 @@ mod cli_integration {
.expect("success");
assert!(
output.contains("IAM error: Not enough permissions to perform this action"),
"auth level datbase should not be able to access root info: {output}",
"auth level database should not be able to access root info: {output}",
);
}
@ -401,7 +401,7 @@ mod cli_integration {
.expect("success");
assert!(
output.contains("IAM error: Not enough permissions to perform this action"),
"auth level datbase should not be able to access namespace info: {output}",
"auth level database should not be able to access namespace info: {output}",
);
}
@ -416,7 +416,7 @@ mod cli_integration {
.expect("success");
assert!(
output.contains("tables: {"),
"auth level datbase should be able to access database info: {output}"
"auth level database should be able to access database info: {output}"
);
}
@ -453,6 +453,72 @@ mod cli_integration {
}
}
#[test(tokio::test)]
// TODO(gguillemas): Remove this test once the legacy authentication is deprecated in v2.0.0
async fn without_auth_level() {
// Commands with credentials for different auth levels
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
let creds = format!("--user {USER} --pass {PASS}");
let ns = Ulid::new();
let db = Ulid::new();
info!("* Create users with identical credentials at ROOT, NS and DB levels");
{
let args = format!("sql --conn http://{addr} --db {db} --ns {ns} {creds}");
let _ = common::run(&args)
.input(format!("DEFINE USER {USER}_root ON ROOT PASSWORD '{PASS}' ROLES OWNER;
DEFINE USER {USER}_ns ON NAMESPACE PASSWORD '{PASS}' ROLES OWNER;
DEFINE USER {USER}_db ON DATABASE PASSWORD '{PASS}' ROLES OWNER;\n").as_str())
.output()
.expect("success");
}
info!("* Pass root level credentials and access root info");
{
let args = format!(
"sql --conn http://{addr} --db {db} --ns {ns} --user {USER}_root --pass {PASS}"
);
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: {"),
"auth level root should be able to access root info: {output}"
);
}
info!("* Pass namespace level credentials and access namespace info");
{
let args = format!(
"sql --conn http://{addr} --db {db} --ns {ns} --user {USER}_ns --pass {PASS}"
);
let output = common::run(&args)
.input(format!("USE NS {ns} DB {db}; INFO FOR NS;\n").as_str())
.output();
assert!(
output.clone().unwrap_err().contains("401 Unauthorized"),
"namespace level credentials should not work with CLI authentication: {:?}",
output
);
}
info!("* Pass database level credentials and access database info");
{
let args = format!(
"sql --conn http://{addr} --db {db} --ns {ns} --user {USER}_db --pass {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("401 Unauthorized"),
"database level credentials should not work with CLI authentication: {:?}",
output
);
}
}
#[test(tokio::test)]
async fn with_anon_auth() {
// Commands without credentials when auth is enabled, should fail

View file

@ -215,7 +215,7 @@ pub async fn start_server(
}
if enable_auth_level {
extra_args.push_str(" --enable-auth-level");
extra_args.push_str(" --auth-level-enabled");
}
if !tick_interval.is_zero() {

View file

@ -210,7 +210,7 @@ mod http_integration {
}
#[test(tokio::test)]
// TODO(gguillemas): Remove this test once the legacy basic auth is deprecated in v2.0.0
// TODO(gguillemas): Remove this test once the legacy authentication is deprecated in v2.0.0
async fn basic_auth_legacy() -> Result<(), Box<dyn std::error::Error>> {
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
let url = &format!("http://{addr}/sql");
@ -748,6 +748,231 @@ mod http_integration {
#[test(tokio::test)]
async fn signin_endpoint() -> Result<(), Box<dyn std::error::Error>> {
let (addr, _server) = common::start_server_with_auth_level().await.unwrap();
let url = &format!("http://{addr}/signin");
let ns = Ulid::new().to_string();
let db = Ulid::new().to_string();
// Prepare HTTP client
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("NS", ns.parse()?);
headers.insert("DB", db.parse()?);
headers.insert(header::ACCEPT, "application/json".parse()?);
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_millis(10))
.default_headers(headers)
.build()?;
// Create a DB user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user_db ON DB PASSWORD 'pass_db'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid DB credentials and get the token
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_db",
"pass": "pass_db",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 200, "body: {}", res.text().await?);
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid DB credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_db",
"pass": "invalid_pass",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Create a NS user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user_ns ON NS PASSWORD 'pass_ns'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid NS credentials specifying NS and DB and get the token
// This should fail because authentication will be attempted at DB level
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_ns",
"pass": "pass_ns",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Signin with valid NS credentials specifying NS and get the token
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"user": "user_ns",
"pass": "pass_ns",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 200, "body: {}", res.text().await?);
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid NS credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_ns",
"pass": "invalid_pass",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Create a ROOT user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user_root ON ROOT PASSWORD 'pass_root'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid ROOT credentials specifying NS and DB and get the token
// This should fail because authentication will be attempted at DB level
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_root",
"pass": "pass_root",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Signin with valid ROOT credentials specifying NS and get the token
// This should fail because authentication will be attempted at NS level
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"user": "user_root",
"pass": "pass_root",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Signin with valid ROOT credentials without specifying NS nor DB and get the token
{
let req_body = serde_json::to_string(
json!({
"user": "user_root",
"pass": "pass_root",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 200, "body: {}", res.text().await?);
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid ROOT credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_root",
"pass": "invalid_pass",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
Ok(())
}
#[test(tokio::test)]
// TODO(gguillemas): Remove this test once the legacy authentication is deprecated in v2.0.0
async fn signin_endpoint_legacy() -> Result<(), Box<dyn std::error::Error>> {
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
let url = &format!("http://{addr}/signin");
@ -764,25 +989,25 @@ mod http_integration {
.default_headers(headers)
.build()?;
// Create a user
// Create a DB user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user ON DB PASSWORD 'pass'"#)
.body(r#"DEFINE USER user_db ON DB PASSWORD 'pass_db'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid credentials and get the token
// Signin with valid DB credentials and get the token
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user",
"pass": "pass",
"user": "user_db",
"pass": "pass_db",
})
.as_object()
.unwrap(),
@ -796,13 +1021,113 @@ mod http_integration {
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid credentials returns 403
// Signin with invalid DB credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user",
"user": "user_db",
"pass": "invalid_pass",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Create a NS user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user_ns ON ROOT PASSWORD 'pass_ns'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid NS credentials specifying NS and DB and get the token
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_ns",
"pass": "pass_ns",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 200, "body: {}", res.text().await?);
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid NS credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_ns",
"pass": "invalid_pass",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 401, "body: {}", res.text().await?);
}
// Create a ROOT user
{
let res = client
.post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS))
.body(r#"DEFINE USER user_root ON ROOT PASSWORD 'pass_root'"#)
.send()
.await?;
assert!(res.status().is_success(), "body: {}", res.text().await?);
}
// Signin with valid ROOT credentials specifying NS and DB and get the token
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_root",
"pass": "pass_root",
})
.as_object()
.unwrap(),
)
.unwrap();
let res = client.post(url).body(req_body).send().await?;
assert_eq!(res.status(), 200, "body: {}", res.text().await?);
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
assert!(!body["token"].as_str().unwrap().to_string().is_empty(), "body: {}", body);
}
// Signin with invalid ROOT credentials returns 401
{
let req_body = serde_json::to_string(
json!({
"ns": ns,
"db": db,
"user": "user_root",
"pass": "invalid_pass",
})
.as_object()