Require explicit authentication level (#4089)
This commit is contained in:
parent
3ccadb0740
commit
bf702b0d67
17 changed files with 92 additions and 962 deletions
|
@ -1,4 +1,4 @@
|
|||
use super::verify::{verify_creds_legacy, verify_db_creds, verify_ns_creds, verify_root_creds};
|
||||
use super::verify::{verify_db_creds, verify_ns_creds, verify_root_creds};
|
||||
use super::{Actor, Level};
|
||||
use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
|
||||
use crate::dbs::Session;
|
||||
|
@ -210,16 +210,7 @@ pub async fn db_user(
|
|||
user: String,
|
||||
pass: String,
|
||||
) -> Result<Option<String>, Error> {
|
||||
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 {
|
||||
match verify_db_creds(kvs, &ns, &db, &user, &pass).await {
|
||||
Ok(u) => {
|
||||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
|
@ -265,16 +256,7 @@ pub async fn ns_user(
|
|||
user: String,
|
||||
pass: String,
|
||||
) -> Result<Option<String>, Error> {
|
||||
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 {
|
||||
match verify_ns_creds(kvs, &ns, &user, &pass).await {
|
||||
Ok(u) => {
|
||||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
|
@ -318,16 +300,7 @@ pub async fn root_user(
|
|||
user: String,
|
||||
pass: String,
|
||||
) -> Result<Option<String>, Error> {
|
||||
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 {
|
||||
match verify_root_creds(kvs, &user, &pass).await {
|
||||
Ok(u) => {
|
||||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
|
|
|
@ -131,43 +131,6 @@ pub async fn basic(
|
|||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
) -> Result<(), Error> {
|
||||
// Log the authentication type
|
||||
trace!("Attempting legacy basic authentication");
|
||||
|
||||
match verify_creds_legacy(kvs, session.ns.as_ref(), session.db.as_ref(), user, pass).await {
|
||||
Ok((au, _)) if au.is_root() => {
|
||||
debug!("Authenticated as root user '{}'", user);
|
||||
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
|
||||
session.exp = None;
|
||||
session.au = Arc::new(au);
|
||||
Ok(())
|
||||
}
|
||||
Ok((au, _)) if au.is_ns() => {
|
||||
debug!("Authenticated as namespace user '{}'", user);
|
||||
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
|
||||
session.exp = None;
|
||||
session.au = Arc::new(au);
|
||||
Ok(())
|
||||
}
|
||||
Ok((au, _)) if au.is_db() => {
|
||||
debug!("Authenticated as database user '{}'", user);
|
||||
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
|
||||
session.exp = None;
|
||||
session.au = Arc::new(au);
|
||||
Ok(())
|
||||
}
|
||||
Ok(_) => Err(Error::InvalidAuth),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Result<(), Error> {
|
||||
// Log the authentication type
|
||||
trace!("Attempting token authentication");
|
||||
|
@ -520,48 +483,6 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
|
|||
}
|
||||
}
|
||||
|
||||
// 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>,
|
||||
db: Option<&String>,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
) -> Result<(Auth, DefineUserStatement), Error> {
|
||||
if user.is_empty() || pass.is_empty() {
|
||||
return Err(Error::InvalidAuth);
|
||||
}
|
||||
|
||||
// Try to authenticate as a ROOT user
|
||||
match verify_root_creds(ds, user, pass).await {
|
||||
Ok(u) => Ok(((&u, Level::Root).into(), u)),
|
||||
Err(_) => {
|
||||
// Try to authenticate as a NS user
|
||||
match ns {
|
||||
Some(ns) => {
|
||||
match verify_ns_creds(ds, ns, user, pass).await {
|
||||
Ok(u) => Ok(((&u, Level::Namespace(ns.to_owned())).into(), u)),
|
||||
Err(_) => {
|
||||
// Try to authenticate as a DB user
|
||||
match db {
|
||||
Some(db) => match verify_db_creds(ds, ns, db, user, pass).await {
|
||||
Ok(u) => Ok((
|
||||
(&u, Level::Database(ns.to_owned(), db.to_owned())).into(),
|
||||
u,
|
||||
)),
|
||||
Err(_) => Err(Error::InvalidAuth),
|
||||
},
|
||||
None => Err(Error::InvalidAuth),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(Error::InvalidAuth),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -71,9 +71,6 @@ 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
|
||||
|
@ -365,8 +362,6 @@ 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,
|
||||
|
@ -427,13 +422,6 @@ 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;
|
||||
|
@ -469,12 +457,6 @@ 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
|
||||
}
|
||||
|
||||
/// Does the datastore allow connections to a network target?
|
||||
#[cfg(feature = "jwks")]
|
||||
pub(crate) fn allows_network_target(&self, net_target: &NetTarget) -> bool {
|
||||
|
|
|
@ -28,8 +28,8 @@ use crate::api::Surreal;
|
|||
use crate::dbs::Status;
|
||||
use crate::headers::AUTH_DB;
|
||||
use crate::headers::AUTH_NS;
|
||||
use crate::headers::DB_LEGACY;
|
||||
use crate::headers::NS_LEGACY;
|
||||
use crate::headers::DB;
|
||||
use crate::headers::NS;
|
||||
use crate::method::Stats;
|
||||
use crate::opt::IntoEndpoint;
|
||||
use crate::sql::from_value;
|
||||
|
@ -369,7 +369,7 @@ async fn router(
|
|||
let ns = match ns {
|
||||
Some(ns) => match HeaderValue::try_from(&ns) {
|
||||
Ok(ns) => {
|
||||
request = request.header(&NS_LEGACY, &ns);
|
||||
request = request.header(&NS, &ns);
|
||||
Some(ns)
|
||||
}
|
||||
Err(_) => {
|
||||
|
@ -381,7 +381,7 @@ async fn router(
|
|||
let db = match db {
|
||||
Some(db) => match HeaderValue::try_from(&db) {
|
||||
Ok(db) => {
|
||||
request = request.header(&DB_LEGACY, &db);
|
||||
request = request.header(&DB, &db);
|
||||
Some(db)
|
||||
}
|
||||
Err(_) => {
|
||||
|
@ -393,10 +393,10 @@ async fn router(
|
|||
request = request.auth(auth).body("RETURN true");
|
||||
take(true, request).await?;
|
||||
if let Some(ns) = ns {
|
||||
headers.insert(&NS_LEGACY, ns);
|
||||
headers.insert(&NS, ns);
|
||||
}
|
||||
if let Some(db) = db {
|
||||
headers.insert(&DB_LEGACY, db);
|
||||
headers.insert(&DB, db);
|
||||
}
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
|
|
|
@ -3,12 +3,8 @@
|
|||
use reqwest::header::HeaderName;
|
||||
|
||||
pub static ID: HeaderName = HeaderName::from_static("surreal-id");
|
||||
pub static ID_LEGACY: HeaderName = HeaderName::from_static("id");
|
||||
pub static NS: HeaderName = HeaderName::from_static("surreal-ns");
|
||||
pub static NS_LEGACY: HeaderName = HeaderName::from_static("ns");
|
||||
pub static DB: HeaderName = HeaderName::from_static("surreal-db");
|
||||
pub static DB_LEGACY: HeaderName = HeaderName::from_static("db");
|
||||
pub static AUTH_NS: HeaderName = HeaderName::from_static("surreal-auth-ns");
|
||||
pub static AUTH_DB: HeaderName = HeaderName::from_static("surreal-auth-db");
|
||||
pub static VERSION: HeaderName = HeaderName::from_static("surreal-version");
|
||||
pub static VERSION_LEGACY: HeaderName = HeaderName::from_static("version");
|
||||
|
|
|
@ -23,11 +23,7 @@ pub(crate) struct AuthArguments {
|
|||
requires = "username"
|
||||
)]
|
||||
pub(crate) password: Option<String>,
|
||||
// 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"
|
||||
)]
|
||||
#[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())]
|
||||
pub(crate) auth_level: CredentialsLevel,
|
||||
|
|
|
@ -27,14 +27,6 @@ pub struct StartCommandDbsOptions {
|
|||
#[arg(env = "SURREAL_UNAUTHENTICATED", long = "unauthenticated")]
|
||||
#[arg(default_value_t = false)]
|
||||
unauthenticated: 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,
|
||||
|
@ -216,8 +208,6 @@ pub async fn init(
|
|||
query_timeout,
|
||||
transaction_timeout,
|
||||
unauthenticated,
|
||||
// TODO(gguillemas): Remove this field once the legacy authentication is deprecated in v2.0.0
|
||||
auth_level_enabled,
|
||||
caps,
|
||||
temporary_directory,
|
||||
}: StartCommandDbsOptions,
|
||||
|
@ -238,11 +228,6 @@ pub async fn init(
|
|||
if unauthenticated {
|
||||
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}");
|
||||
|
@ -256,7 +241,6 @@ pub async fn init(
|
|||
.with_query_timeout(query_timeout)
|
||||
.with_transaction_timeout(transaction_timeout)
|
||||
.with_auth_enabled(!unauthenticated)
|
||||
.with_auth_level_enabled(auth_level_enabled)
|
||||
.with_capabilities(caps);
|
||||
|
||||
let mut dbs = match temporary_directory {
|
||||
|
|
|
@ -11,7 +11,7 @@ use http::{request::Parts, StatusCode};
|
|||
use hyper::{Request, Response};
|
||||
use surrealdb::{
|
||||
dbs::Session,
|
||||
iam::verify::{basic, basic_legacy, token},
|
||||
iam::verify::{basic, token},
|
||||
};
|
||||
use tower_http::auth::AsyncAuthorizeRequest;
|
||||
|
||||
|
@ -20,9 +20,8 @@ use crate::{dbs::DB, err::Error};
|
|||
use super::{
|
||||
client_ip::ExtractClientIP,
|
||||
headers::{
|
||||
parse_typed_header, SurrealAuthDatabase, SurrealAuthNamespace, SurrealDatabase,
|
||||
SurrealDatabaseLegacy, SurrealId, SurrealIdLegacy, SurrealNamespace,
|
||||
SurrealNamespaceLegacy,
|
||||
parse_typed_header, SurrealAuthDatabase, SurrealAuthNamespace, SurrealDatabase, SurrealId,
|
||||
SurrealNamespace,
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
|
@ -88,34 +87,18 @@ async fn check_auth(parts: &mut Parts) -> Result<Session, Error> {
|
|||
None
|
||||
};
|
||||
|
||||
// Extract the session id from the headers. If not found, fallback to the legacy header name.
|
||||
let id = match parse_typed_header::<SurrealId>(parts.extract::<TypedHeader<SurrealId>>().await)
|
||||
{
|
||||
Ok(None) => parse_typed_header::<SurrealIdLegacy>(
|
||||
parts.extract::<TypedHeader<SurrealIdLegacy>>().await,
|
||||
),
|
||||
res => res,
|
||||
}?;
|
||||
// Extract the session id from the headers.
|
||||
let id = parse_typed_header::<SurrealId>(parts.extract::<TypedHeader<SurrealId>>().await)?;
|
||||
|
||||
// Extract the namespace from the headers. If not found, fallback to the legacy header name.
|
||||
let ns = match parse_typed_header::<SurrealNamespace>(
|
||||
// Extract the namespace from the headers.
|
||||
let ns = parse_typed_header::<SurrealNamespace>(
|
||||
parts.extract::<TypedHeader<SurrealNamespace>>().await,
|
||||
) {
|
||||
Ok(None) => parse_typed_header::<SurrealNamespaceLegacy>(
|
||||
parts.extract::<TypedHeader<SurrealNamespaceLegacy>>().await,
|
||||
),
|
||||
res => res,
|
||||
}?;
|
||||
)?;
|
||||
|
||||
// Extract the database from the headers. If not found, fallback to the legacy header name.
|
||||
let db = match parse_typed_header::<SurrealDatabase>(
|
||||
// Extract the database from the headers.
|
||||
let db = parse_typed_header::<SurrealDatabase>(
|
||||
parts.extract::<TypedHeader<SurrealDatabase>>().await,
|
||||
) {
|
||||
Ok(None) => parse_typed_header::<SurrealDatabaseLegacy>(
|
||||
parts.extract::<TypedHeader<SurrealDatabaseLegacy>>().await,
|
||||
),
|
||||
res => res,
|
||||
}?;
|
||||
)?;
|
||||
|
||||
// Extract the authentication namespace and database from the headers.
|
||||
let auth_ns = parse_typed_header::<SurrealAuthNamespace>(
|
||||
|
@ -143,7 +126,6 @@ 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 {
|
||||
if kvs.is_auth_level_enabled() {
|
||||
basic(
|
||||
kvs,
|
||||
&mut session,
|
||||
|
@ -153,9 +135,6 @@ async fn check_auth(parts: &mut Parts) -> Result<Session, Error> {
|
|||
auth_db.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
basic_legacy(kvs, &mut session, au.username(), au.password()).await?;
|
||||
}
|
||||
};
|
||||
|
||||
// If Token authentication data was supplied
|
||||
|
|
|
@ -50,53 +50,3 @@ impl From<&SurrealDatabase> for HeaderValue {
|
|||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Legacy header
|
||||
//
|
||||
static DB_LEGACY_HEADER: HeaderName = HeaderName::from_static("db");
|
||||
|
||||
pub struct SurrealDatabaseLegacy(String);
|
||||
|
||||
impl Header for SurrealDatabaseLegacy {
|
||||
fn name() -> &'static HeaderName {
|
||||
&DB_LEGACY_HEADER
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||
where
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
let value = values.next().ok_or_else(headers::Error::invalid)?;
|
||||
let value = value.to_str().map_err(|_| headers::Error::invalid())?.to_string();
|
||||
|
||||
Ok(SurrealDatabaseLegacy(value))
|
||||
}
|
||||
|
||||
fn encode<E>(&self, values: &mut E)
|
||||
where
|
||||
E: Extend<HeaderValue>,
|
||||
{
|
||||
values.extend(std::iter::once(self.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for SurrealDatabaseLegacy {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SurrealDatabaseLegacy> for HeaderValue {
|
||||
fn from(value: SurrealDatabaseLegacy) -> Self {
|
||||
HeaderValue::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SurrealDatabaseLegacy> for HeaderValue {
|
||||
fn from(value: &SurrealDatabaseLegacy) -> Self {
|
||||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,52 +50,3 @@ impl From<&SurrealId> for HeaderValue {
|
|||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Legacy header
|
||||
//
|
||||
static ID_LEGACY_HEADER: HeaderName = HeaderName::from_static("id");
|
||||
pub struct SurrealIdLegacy(String);
|
||||
|
||||
impl Header for SurrealIdLegacy {
|
||||
fn name() -> &'static HeaderName {
|
||||
&ID_LEGACY_HEADER
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||
where
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
let value = values.next().ok_or_else(headers::Error::invalid)?;
|
||||
let value = value.to_str().map_err(|_| headers::Error::invalid())?.to_string();
|
||||
|
||||
Ok(SurrealIdLegacy(value))
|
||||
}
|
||||
|
||||
fn encode<E>(&self, values: &mut E)
|
||||
where
|
||||
E: Extend<HeaderValue>,
|
||||
{
|
||||
values.extend(std::iter::once(self.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for SurrealIdLegacy {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SurrealIdLegacy> for HeaderValue {
|
||||
fn from(value: SurrealIdLegacy) -> Self {
|
||||
HeaderValue::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SurrealIdLegacy> for HeaderValue {
|
||||
fn from(value: &SurrealIdLegacy) -> Self {
|
||||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ pub use accept::Accept;
|
|||
pub use auth_db::SurrealAuthDatabase;
|
||||
pub use auth_ns::SurrealAuthNamespace;
|
||||
pub use content_type::ContentType;
|
||||
pub use db::{SurrealDatabase, SurrealDatabaseLegacy};
|
||||
pub use id::{SurrealId, SurrealIdLegacy};
|
||||
pub use ns::{SurrealNamespace, SurrealNamespaceLegacy};
|
||||
pub use db::SurrealDatabase;
|
||||
pub use id::SurrealId;
|
||||
pub use ns::SurrealNamespace;
|
||||
|
||||
pub fn add_version_header() -> SetResponseHeaderLayer<HeaderValue> {
|
||||
let val = format!("{PKG_NAME}-{}", *PKG_VERSION);
|
||||
|
|
|
@ -50,53 +50,3 @@ impl From<&SurrealNamespace> for HeaderValue {
|
|||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Legacy header
|
||||
//
|
||||
static NS_LEGACY_HEADER: HeaderName = HeaderName::from_static("ns");
|
||||
|
||||
pub struct SurrealNamespaceLegacy(String);
|
||||
|
||||
impl Header for SurrealNamespaceLegacy {
|
||||
fn name() -> &'static HeaderName {
|
||||
&NS_LEGACY_HEADER
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||
where
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
let value = values.next().ok_or_else(headers::Error::invalid)?;
|
||||
let value = value.to_str().map_err(|_| headers::Error::invalid())?.to_string();
|
||||
|
||||
Ok(SurrealNamespaceLegacy(value))
|
||||
}
|
||||
|
||||
fn encode<E>(&self, values: &mut E)
|
||||
where
|
||||
E: Extend<HeaderValue>,
|
||||
{
|
||||
values.extend(std::iter::once(self.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for SurrealNamespaceLegacy {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SurrealNamespaceLegacy> for HeaderValue {
|
||||
fn from(value: SurrealNamespaceLegacy) -> Self {
|
||||
HeaderValue::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SurrealNamespaceLegacy> for HeaderValue {
|
||||
fn from(value: &SurrealNamespaceLegacy) -> Self {
|
||||
HeaderValue::from_str(value.0.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ use http::header;
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use surrealdb::headers::{AUTH_DB, AUTH_NS, DB, DB_LEGACY, ID, ID_LEGACY, NS, NS_LEGACY};
|
||||
use surrealdb::headers::{AUTH_DB, AUTH_NS, DB, ID, NS};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::add_extension::AddExtensionLayer;
|
||||
|
@ -108,10 +108,6 @@ pub async fn init(ct: CancellationToken) -> Result<(), Error> {
|
|||
ID.clone(),
|
||||
AUTH_NS.clone(),
|
||||
AUTH_DB.clone(),
|
||||
// TODO(gguillemas): Remove these headers once the legacy authentication is deprecated in v2.0.0
|
||||
NS_LEGACY.clone(),
|
||||
DB_LEGACY.clone(),
|
||||
ID_LEGACY.clone(),
|
||||
];
|
||||
|
||||
#[cfg(not(feature = "http-compression"))]
|
||||
|
@ -125,10 +121,6 @@ pub async fn init(ct: CancellationToken) -> Result<(), Error> {
|
|||
ID.clone(),
|
||||
AUTH_NS.clone(),
|
||||
AUTH_DB.clone(),
|
||||
// TODO(gguillemas): Remove these headers once the legacy authentication is deprecated in v2.0.0
|
||||
NS_LEGACY.clone(),
|
||||
DB_LEGACY.clone(),
|
||||
ID_LEGACY.clone(),
|
||||
];
|
||||
|
||||
let service = service
|
||||
|
|
|
@ -306,7 +306,7 @@ mod cli_integration {
|
|||
#[test(tokio::test)]
|
||||
async fn with_auth_level() {
|
||||
// Commands with credentials for different auth levels
|
||||
let (addr, mut server) = common::start_server_with_auth_level().await.unwrap();
|
||||
let (addr, mut server) = common::start_server_with_defaults().await.unwrap();
|
||||
let creds = format!("--user {USER} --pass {PASS}");
|
||||
let ns = Ulid::new();
|
||||
let db = Ulid::new();
|
||||
|
@ -488,74 +488,6 @@ mod cli_integration {
|
|||
server.finish().unwrap();
|
||||
}
|
||||
|
||||
#[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, mut server) = common::start_server_with_defaults().await.unwrap();
|
||||
let creds = format!("--user {USER} --pass {PASS}");
|
||||
// Prefix with 'a' so that we don't start with a number and cause a parsing error
|
||||
let ns = format!("a{}", Ulid::new());
|
||||
let db = format!("a{}", 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
|
||||
);
|
||||
}
|
||||
server.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn with_anon_auth() {
|
||||
// Commands without credentials when auth is enabled, should fail
|
||||
|
|
|
@ -150,7 +150,6 @@ pub struct StartServerArguments {
|
|||
pub auth: bool,
|
||||
pub tls: bool,
|
||||
pub wait_is_ready: bool,
|
||||
pub enable_auth_level: bool,
|
||||
pub tick_interval: time::Duration,
|
||||
pub temporary_directory: Option<String>,
|
||||
pub args: String,
|
||||
|
@ -163,7 +162,6 @@ impl Default for StartServerArguments {
|
|||
auth: true,
|
||||
tls: false,
|
||||
wait_is_ready: true,
|
||||
enable_auth_level: false,
|
||||
tick_interval: time::Duration::new(1, 0),
|
||||
temporary_directory: None,
|
||||
args: "--allow-all".to_string(),
|
||||
|
@ -179,14 +177,6 @@ pub async fn start_server_without_auth() -> Result<(String, Child), Box<dyn Erro
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn start_server_with_auth_level() -> Result<(String, Child), Box<dyn Error>> {
|
||||
start_server(StartServerArguments {
|
||||
enable_auth_level: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_server_with_defaults() -> Result<(String, Child), Box<dyn Error>> {
|
||||
start_server(StartServerArguments::default()).await
|
||||
}
|
||||
|
@ -207,7 +197,6 @@ pub async fn start_server(
|
|||
auth,
|
||||
tls,
|
||||
wait_is_ready,
|
||||
enable_auth_level,
|
||||
tick_interval,
|
||||
temporary_directory,
|
||||
args,
|
||||
|
@ -234,10 +223,6 @@ pub async fn start_server(
|
|||
extra_args.push_str(" --unauthenticated");
|
||||
}
|
||||
|
||||
if enable_auth_level {
|
||||
extra_args.push_str(" --auth-level-enabled");
|
||||
}
|
||||
|
||||
if !tick_interval.is_zero() {
|
||||
let sec = tick_interval.as_secs();
|
||||
extra_args.push_str(format!(" --tick-interval {sec}s").as_str());
|
||||
|
|
|
@ -15,15 +15,15 @@ mod http_integration {
|
|||
|
||||
#[test(tokio::test)]
|
||||
async fn basic_auth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (addr, _server) = common::start_server_with_auth_level().await.unwrap();
|
||||
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
|
||||
let url = &format!("http://{addr}/sql");
|
||||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let ns = Ulid::new().to_string();
|
||||
let db = Ulid::new().to_string();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -209,295 +209,6 @@ mod http_integration {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
// 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");
|
||||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let ns = Ulid::new().to_string();
|
||||
let db = Ulid::new().to_string();
|
||||
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()?;
|
||||
|
||||
// Request without credentials, gives an anonymous session
|
||||
{
|
||||
let res = client.post(url).body("CREATE foo").send().await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body = res.text().await?;
|
||||
assert!(body.contains("Not enough permissions"), "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with invalid credentials, returns 401
|
||||
{
|
||||
let res =
|
||||
client.post(url).basic_auth("user", Some("pass")).body("CREATE foo").send().await?;
|
||||
assert_eq!(res.status(), 401);
|
||||
}
|
||||
|
||||
// Request with valid root credentials, gives a ROOT session
|
||||
{
|
||||
let res =
|
||||
client.post(url).basic_auth(USER, Some(PASS)).body("CREATE foo").send().await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body = res.text().await?;
|
||||
assert!(body.contains(r#"[{"result":[{"id":"foo:"#), "body: {}", body);
|
||||
}
|
||||
|
||||
// Prepare users with identical credentials on ROOT, NAMESPACE and DATABASE levels
|
||||
{
|
||||
let res =
|
||||
client.post(url).basic_auth(USER, Some(PASS))
|
||||
.body(format!("DEFINE USER {USER}_root ON ROOT PASSWORD '{PASS}' ROLES OWNER;
|
||||
DEFINE USER {USER}_root ON NAMESPACE PASSWORD '{PASS}' ROLES OWNER;
|
||||
DEFINE USER {USER}_root ON DATABASE PASSWORD '{PASS}' ROLES OWNER",
|
||||
)).send().await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
}
|
||||
|
||||
// Prepare users with identical credentials on NAMESPACE and DATABASE levels
|
||||
{
|
||||
let res =
|
||||
client.post(url).basic_auth(USER, Some(PASS))
|
||||
.body(format!("DEFINE USER {USER}_ns ON NAMESPACE PASSWORD '{PASS}' ROLES OWNER;
|
||||
DEFINE USER {USER}_ns ON DATABASE PASSWORD '{PASS}' ROLES OWNER",
|
||||
)).send().await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
}
|
||||
|
||||
// Prepare users with on DATABASE level
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(USER, Some(PASS))
|
||||
.body(format!("DEFINE USER {USER}_db ON DATABASE PASSWORD '{PASS}' ROLES OWNER",))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
}
|
||||
|
||||
// Request with ROOT level shared credentials to access ROOT, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "root"), Some(PASS))
|
||||
.body("INFO FOR ROOT")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with ROOT level shared credentials to access NS, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "root"), Some(PASS))
|
||||
.body("INFO FOR NS")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with ROOT level shared credentials to access NS, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "root"), Some(PASS))
|
||||
.body("INFO FOR DB")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with NS level shared credentials to access ROOT, returns 200 but fails
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "ns"), Some(PASS))
|
||||
.body("INFO FOR ROOT")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "ERR", "body: {}", body);
|
||||
assert_eq!(
|
||||
body[0]["result"], "IAM error: Not enough permissions to perform this action",
|
||||
"body: {}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
// Request with NS level shared credentials to access NS, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "ns"), Some(PASS))
|
||||
.body("INFO FOR NS")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with NS level shared credentials to access DB, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "ns"), Some(PASS))
|
||||
.body("INFO FOR DB")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with DB level shared credentials to access ROOT, returns 200 but fails
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "db"), Some(PASS))
|
||||
.body("INFO FOR ROOT")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "ERR", "body: {}", body);
|
||||
assert_eq!(
|
||||
body[0]["result"], "IAM error: Not enough permissions to perform this action",
|
||||
"body: {}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
// Request with DB level shared credentials to access NS, returns 200 but fails
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "db"), Some(PASS))
|
||||
.body("INFO FOR NS")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "ERR", "body: {}", body);
|
||||
assert_eq!(
|
||||
body[0]["result"], "IAM error: Not enough permissions to perform this action",
|
||||
"body: {}",
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
// Request with DB level shared credentials to access DB, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(format!("{}_{}", USER, "db"), Some(PASS))
|
||||
.body("INFO FOR DB")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Prepare users with identical name but different password on ROOT, NAMESPACE and DATABASE levels
|
||||
let shared_user_name = format!("{}_{}", USER, "all");
|
||||
let shared_user_pass_root = format!("{}_{}", PASS, "root");
|
||||
let shared_user_pass_ns = format!("{}_{}", PASS, "ns");
|
||||
let shared_user_pass_db = format!("{}_{}", PASS, "db");
|
||||
{
|
||||
let res =
|
||||
client.post(url).basic_auth(USER, Some(PASS))
|
||||
.body(format!("DEFINE USER {shared_user_name} ON ROOT PASSWORD '{shared_user_pass_root}' ROLES OWNER;
|
||||
DEFINE USER {shared_user_name} ON NAMESPACE PASSWORD '{shared_user_pass_ns}' ROLES OWNER;
|
||||
DEFINE USER {shared_user_name} ON DATABASE PASSWORD '{shared_user_pass_db}' ROLES OWNER",
|
||||
)).send().await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
}
|
||||
|
||||
// Request with shared user with password for ROOT to access ROOT, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(&shared_user_name, Some(&shared_user_pass_root))
|
||||
.body("INFO FOR ROOT")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with shared user with password for NS to access ROOT, returns 200 but fails
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(&shared_user_name, Some(&shared_user_pass_ns))
|
||||
.body("INFO FOR ROOT")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "ERR", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with shared user with password for NS to access NS, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(&shared_user_name, Some(&shared_user_pass_ns))
|
||||
.body("INFO FOR NS")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with shared user with password for DB to access NS, returns 200 but fails
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(&shared_user_name, Some(&shared_user_pass_db))
|
||||
.body("INFO FOR NS")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "ERR", "body: {}", body);
|
||||
}
|
||||
|
||||
// Request with shared user with password for DB to access DB, returns 200 and succeeds
|
||||
{
|
||||
let res = client
|
||||
.post(url)
|
||||
.basic_auth(&shared_user_name, Some(&shared_user_pass_db))
|
||||
.body("INFO FOR DB")
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(res.status(), 200);
|
||||
let body: serde_json::Value = serde_json::from_str(&res.text().await?).unwrap();
|
||||
assert_eq!(body[0]["status"], "OK", "body: {}", body);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn bearer_auth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
|
||||
|
@ -508,8 +219,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -594,8 +305,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -648,8 +359,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -720,8 +431,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -748,7 +459,7 @@ 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 (addr, _server) = common::start_server_with_defaults().await.unwrap();
|
||||
let url = &format!("http://{addr}/signin");
|
||||
|
||||
let ns = Ulid::new().to_string();
|
||||
|
@ -756,8 +467,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -970,178 +681,6 @@ mod http_integration {
|
|||
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");
|
||||
|
||||
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 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()
|
||||
.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)]
|
||||
async fn signup_endpoint() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (addr, _server) = common::start_server_with_defaults().await.unwrap();
|
||||
|
@ -1152,8 +691,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1214,8 +753,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
|
@ -1328,8 +867,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
headers.insert(header::ACCEPT_ENCODING, "gzip".parse()?);
|
||||
|
||||
|
@ -1361,8 +900,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1437,8 +976,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1525,8 +1064,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1590,8 +1129,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1659,8 +1198,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1728,8 +1267,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1780,8 +1319,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1819,8 +1358,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1916,8 +1455,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -1992,8 +1531,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -2069,8 +1608,8 @@ mod http_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", Ulid::new().to_string().parse()?);
|
||||
headers.insert("DB", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-ns", Ulid::new().to_string().parse()?);
|
||||
headers.insert("surreal-db", Ulid::new().to_string().parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
|
|
@ -55,8 +55,8 @@ mod ml_integration {
|
|||
let body = Body::wrap_stream(generator);
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(1))
|
||||
.default_headers(headers)
|
||||
|
@ -93,8 +93,8 @@ mod ml_integration {
|
|||
let body = Body::wrap_stream(generator);
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(1))
|
||||
.default_headers(headers)
|
||||
|
@ -128,8 +128,8 @@ mod ml_integration {
|
|||
let body = Body::wrap_stream(generator);
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(1))
|
||||
.default_headers(headers)
|
||||
|
@ -160,8 +160,8 @@ mod ml_integration {
|
|||
let body = Body::wrap_stream(generator);
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(1))
|
||||
.default_headers(headers)
|
||||
|
@ -193,8 +193,8 @@ mod ml_integration {
|
|||
let body = Body::wrap_stream(generator);
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(1))
|
||||
.default_headers(headers)
|
||||
|
@ -227,8 +227,8 @@ mod ml_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
@ -263,8 +263,8 @@ mod ml_integration {
|
|||
|
||||
// Prepare HTTP client
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("NS", ns.parse()?);
|
||||
headers.insert("DB", db.parse()?);
|
||||
headers.insert("surreal-ns", ns.parse()?);
|
||||
headers.insert("surreal-db", db.parse()?);
|
||||
headers.insert(header::ACCEPT, "application/json".parse()?);
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_millis(10))
|
||||
|
|
Loading…
Reference in a new issue