Improve definition of different durations for authentication (#4119)
This commit is contained in:
parent
bf218d9363
commit
20e07a4f79
24 changed files with 1295 additions and 296 deletions
|
@ -10,7 +10,7 @@ use crate::kvs::{Datastore, LockType::*, TransactionType::*};
|
|||
use crate::sql::AccessType;
|
||||
use crate::sql::Object;
|
||||
use crate::sql::Value;
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
@ -148,8 +148,7 @@ pub async fn db_access(
|
|||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
// Token expiration is derived from issuer duration
|
||||
exp: expiration(iss.duration)?,
|
||||
exp: expiration(av.duration.token)?,
|
||||
jti: Some(Uuid::new_v4().to_string()),
|
||||
ns: Some(ns.to_owned()),
|
||||
db: Some(db.to_owned()),
|
||||
|
@ -168,8 +167,7 @@ pub async fn db_access(
|
|||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.rd = Some(Value::from(rid.to_owned()));
|
||||
// Session expiration is derived from record access duration
|
||||
session.exp = expiration(at.duration)?;
|
||||
session.exp = expiration(av.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
rid.to_string(),
|
||||
Default::default(),
|
||||
|
@ -215,12 +213,11 @@ pub async fn db_user(
|
|||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
// Create the authentication claim
|
||||
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
|
||||
let val = Claims {
|
||||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp,
|
||||
exp: expiration(u.duration.token)?,
|
||||
jti: Some(Uuid::new_v4().to_string()),
|
||||
ns: Some(ns.to_owned()),
|
||||
db: Some(db.to_owned()),
|
||||
|
@ -235,7 +232,7 @@ pub async fn db_user(
|
|||
session.tk = Some(val.into());
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
|
||||
// Check the authentication token
|
||||
match enc {
|
||||
|
@ -260,12 +257,11 @@ pub async fn ns_user(
|
|||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
// Create the authentication claim
|
||||
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
|
||||
let val = Claims {
|
||||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp,
|
||||
exp: expiration(u.duration.token)?,
|
||||
jti: Some(Uuid::new_v4().to_string()),
|
||||
ns: Some(ns.to_owned()),
|
||||
id: Some(user),
|
||||
|
@ -278,7 +274,7 @@ pub async fn ns_user(
|
|||
// Set the authentication on the session
|
||||
session.tk = Some(val.into());
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
|
||||
// Check the authentication token
|
||||
match enc {
|
||||
|
@ -303,12 +299,11 @@ pub async fn root_user(
|
|||
// Create the authentication key
|
||||
let key = EncodingKey::from_secret(u.code.as_ref());
|
||||
// Create the authentication claim
|
||||
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
|
||||
let val = Claims {
|
||||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp,
|
||||
exp: expiration(u.duration.token)?,
|
||||
jti: Some(Uuid::new_v4().to_string()),
|
||||
id: Some(user),
|
||||
..Claims::default()
|
||||
|
@ -319,7 +314,7 @@ pub async fn root_user(
|
|||
let enc = encode(&HEADER, &val, &key);
|
||||
// Set the authentication on the session
|
||||
session.tk = Some(val.into());
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Root).into());
|
||||
// Check the authentication token
|
||||
match enc {
|
||||
|
@ -337,6 +332,8 @@ pub async fn root_user(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::iam::Role;
|
||||
use chrono::Duration;
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -347,7 +344,7 @@ mod tests {
|
|||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -356,7 +353,9 @@ mod tests {
|
|||
name: $user,
|
||||
pass: crypto::argon2::generate($pass)
|
||||
}
|
||||
);
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:test CONTENT {
|
||||
name: 'user',
|
||||
|
@ -400,14 +399,14 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Expiration should always be set for tokens issued by SurrealDB
|
||||
// Expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
|
||||
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to follow access method duration"
|
||||
"Session expiration is expected to follow the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -417,7 +416,7 @@ mod tests {
|
|||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -426,7 +425,9 @@ mod tests {
|
|||
name: $user,
|
||||
pass: crypto::argon2::generate($pass)
|
||||
}
|
||||
);
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:test CONTENT {
|
||||
name: 'user',
|
||||
|
@ -464,7 +465,6 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_signin_record_with_jwt_issuer() {
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
// Test with correct credentials
|
||||
{
|
||||
let public_key = r#"-----BEGIN PUBLIC KEY-----
|
||||
|
@ -510,7 +510,6 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
&format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
DURATION 1h
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -521,7 +520,8 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}}
|
||||
)
|
||||
WITH JWT ALGORITHM RS256 KEY '{public_key}'
|
||||
WITH ISSUER KEY '{private_key}' DURATION 15m
|
||||
WITH ISSUER KEY '{private_key}'
|
||||
DURATION FOR SESSION 2h, FOR TOKEN 15m
|
||||
;
|
||||
|
||||
CREATE user:test CONTENT {{
|
||||
|
@ -566,16 +566,16 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Session expiration should always be set for tokens issued by SurrealDB
|
||||
// Session expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_sess_exp =
|
||||
(Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
|
||||
(Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_sess_exp =
|
||||
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
|
||||
(Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_sess_exp && exp < max_sess_exp,
|
||||
"Session expiration is expected to follow access method duration"
|
||||
"Session expiration is expected to follow the defined duration"
|
||||
);
|
||||
|
||||
// Decode token and check that it has been issued as intended
|
||||
|
@ -603,7 +603,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow issuer duration"
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, Some("test".to_string()));
|
||||
|
@ -619,7 +619,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
#[tokio::test]
|
||||
async fn test_signin_db_user() {
|
||||
//
|
||||
// Test without roles defined
|
||||
// Test without roles or expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
|
@ -656,12 +656,87 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}
|
||||
|
||||
//
|
||||
// Test with roles defined
|
||||
// Test without roles and session expiration disabled
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute("DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
|
||||
ds.execute(
|
||||
"DEFINE USER user ON DB PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE",
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
db: Some("test".to_string()),
|
||||
ns: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = db_user(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
"pass".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
|
||||
assert_eq!(sess.db, Some("test".to_string()));
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.au.id(), "user");
|
||||
assert!(sess.au.is_db());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.db, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Test with roles and expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute("DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -691,7 +766,47 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
|
||||
// Expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.db, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid password
|
||||
|
@ -720,7 +835,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
#[tokio::test]
|
||||
async fn test_signin_ns_user() {
|
||||
//
|
||||
// Test without roles defined
|
||||
// Test without roles or expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
|
@ -748,12 +863,78 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}
|
||||
|
||||
//
|
||||
// Test with roles defined
|
||||
// Test without roles and session expiration disabled
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test");
|
||||
ds.execute("DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
|
||||
ds.execute(
|
||||
"DEFINE USER user ON NS PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE",
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res =
|
||||
ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.au.id(), "user");
|
||||
assert!(sess.au.is_ns());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.db, None);
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Test with roles and expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test");
|
||||
ds.execute("DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -774,7 +955,47 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
|
||||
// Expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, Some("test".to_string()));
|
||||
assert_eq!(token_data.claims.db, None);
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid password
|
||||
|
@ -802,7 +1023,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
#[tokio::test]
|
||||
async fn test_signin_root_user() {
|
||||
//
|
||||
// Test without roles defined
|
||||
// Test without roles or expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
|
@ -825,12 +1046,67 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}
|
||||
|
||||
//
|
||||
// Test with roles defined
|
||||
// Test without roles and session expiration disabled
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test");
|
||||
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE", &sess, None).await.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
..Default::default()
|
||||
};
|
||||
let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
|
||||
assert_eq!(sess.au.id(), "user");
|
||||
assert!(sess.au.is_root());
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, None);
|
||||
assert_eq!(token_data.claims.db, None);
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Test with roles and expiration defined
|
||||
//
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner();
|
||||
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
|
||||
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -846,7 +1122,47 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
|
||||
// Expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
// Decode token and check that it has been issued as intended
|
||||
if let Ok(Some(tk)) = res {
|
||||
// Decode token without validation
|
||||
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
|
||||
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||
validation.insecure_disable_signature_validation();
|
||||
validation.validate_nbf = false;
|
||||
validation.validate_exp = false;
|
||||
validation
|
||||
})
|
||||
.unwrap();
|
||||
// Check that token expiration matches the defined duration
|
||||
// Expiration should match the current time plus token duration with some margin
|
||||
let exp = match token_data.claims.exp {
|
||||
Some(exp) => exp,
|
||||
_ => panic!("Token is missing expiration claim"),
|
||||
};
|
||||
let min_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
|
||||
let max_tk_exp =
|
||||
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_tk_exp && exp < max_tk_exp,
|
||||
"Token expiration is expected to follow the defined duration"
|
||||
);
|
||||
// Check required token claims
|
||||
assert_eq!(token_data.claims.ns, None);
|
||||
assert_eq!(token_data.claims.db, None);
|
||||
assert_eq!(token_data.claims.id, Some("user".to_string()));
|
||||
} else {
|
||||
panic!("Token could not be extracted from result")
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid password
|
||||
|
|
|
@ -87,8 +87,7 @@ pub async fn db_access(
|
|||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
// Token expiration is derived from issuer duration
|
||||
exp: expiration(iss.duration)?,
|
||||
exp: expiration(av.duration.token)?,
|
||||
jti: Some(Uuid::new_v4().to_string()),
|
||||
ns: Some(ns.to_owned()),
|
||||
db: Some(db.to_owned()),
|
||||
|
@ -107,8 +106,7 @@ pub async fn db_access(
|
|||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.rd = Some(Value::from(rid.to_owned()));
|
||||
// Session expiration is derived from record access duration
|
||||
session.exp = expiration(at.duration)?;
|
||||
session.exp = expiration(av.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
rid.to_string(),
|
||||
Default::default(),
|
||||
|
@ -156,7 +154,7 @@ mod tests {
|
|||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -165,7 +163,9 @@ mod tests {
|
|||
name: $user,
|
||||
pass: crypto::argon2::generate($pass)
|
||||
}
|
||||
);
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
|
@ -203,14 +203,14 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Expiration should always be set for tokens issued by SurrealDB
|
||||
// Session expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
|
||||
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to follow token duration"
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -220,7 +220,7 @@ mod tests {
|
|||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -229,7 +229,9 @@ mod tests {
|
|||
name: $user,
|
||||
pass: crypto::argon2::generate($pass)
|
||||
}
|
||||
);
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
|
@ -308,7 +310,6 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
&format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
DURATION 1h
|
||||
SIGNIN (
|
||||
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
|
@ -319,7 +320,8 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}}
|
||||
)
|
||||
WITH JWT ALGORITHM RS256 KEY '{public_key}'
|
||||
WITH ISSUER KEY '{private_key}' DURATION 15m
|
||||
WITH ISSUER KEY '{private_key}'
|
||||
DURATION FOR SESSION 2h, FOR TOKEN 15m
|
||||
;
|
||||
|
||||
CREATE user:test CONTENT {{
|
||||
|
@ -368,9 +370,9 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_sess_exp =
|
||||
(Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
|
||||
(Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_sess_exp =
|
||||
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
|
||||
(Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_sess_exp && exp < max_sess_exp,
|
||||
"Session expiration is expected to follow access method duration"
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::dbs::Session;
|
||||
use crate::err::Error;
|
||||
use crate::iam::issue::expiration;
|
||||
#[cfg(feature = "jwks")]
|
||||
use crate::iam::jwks;
|
||||
use crate::iam::{token::Claims, Actor, Auth, Level, Role};
|
||||
use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role};
|
||||
use crate::kvs::{Datastore, LockType::*, TransactionType::*};
|
||||
use crate::sql::access_type::{AccessType, JwtAccessVerify};
|
||||
use crate::sql::{statements::DefineUserStatement, Algorithm, Value};
|
||||
|
@ -99,7 +98,7 @@ pub async fn basic(
|
|||
(Some(ns), Some(db)) => match verify_db_creds(kvs, ns, db, user, pass).await {
|
||||
Ok(u) => {
|
||||
debug!("Authenticated as database user '{}'", user);
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
|
||||
Ok(())
|
||||
}
|
||||
|
@ -109,7 +108,7 @@ pub async fn basic(
|
|||
(Some(ns), None) => match verify_ns_creds(kvs, ns, user, pass).await {
|
||||
Ok(u) => {
|
||||
debug!("Authenticated as namespace user '{}'", user);
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
|
||||
Ok(())
|
||||
}
|
||||
|
@ -119,7 +118,7 @@ pub async fn basic(
|
|||
(None, None) => match verify_root_creds(kvs, user, pass).await {
|
||||
Ok(u) => {
|
||||
debug!("Authenticated as root user '{}'", user);
|
||||
session.exp = expiration(u.session)?;
|
||||
session.exp = expiration(u.duration.session)?;
|
||||
session.au = Arc::new((&u, Level::Root).into());
|
||||
Ok(())
|
||||
}
|
||||
|
@ -195,7 +194,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.rd = Some(Value::from(id.to_owned()));
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
id.to_string(),
|
||||
Default::default(),
|
||||
|
@ -254,7 +253,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
de.name.to_string(),
|
||||
roles,
|
||||
|
@ -287,7 +286,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
id.to_string(),
|
||||
de.roles.iter().map(|r| r.into()).collect(),
|
||||
|
@ -344,7 +343,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au =
|
||||
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns))));
|
||||
Ok(())
|
||||
|
@ -372,7 +371,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Set the session
|
||||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
id.to_string(),
|
||||
de.roles.iter().map(|r| r.into()).collect(),
|
||||
|
@ -401,7 +400,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
trace!("Authenticated to root level with user `{}`", id);
|
||||
// Set the session
|
||||
session.tk = Some(value);
|
||||
session.exp = token_data.claims.exp;
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
id.to_string(),
|
||||
de.roles.iter().map(|r| r.into()).collect(),
|
||||
|
@ -524,7 +523,7 @@ mod tests {
|
|||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
"DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
|
||||
"DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR SESSION 1d",
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
|
@ -546,7 +545,7 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
// Expiration has been set explicitly
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(1) - Duration::seconds(10)).timestamp();
|
||||
|
@ -608,7 +607,7 @@ mod tests {
|
|||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
"DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
|
||||
"DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR SESSION 1d",
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
|
@ -694,7 +693,7 @@ mod tests {
|
|||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
"DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
|
||||
"DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR SESSION 1d",
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
|
@ -761,7 +760,7 @@ mod tests {
|
|||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!("DEFINE ACCESS token ON NS TYPE JWT ALGORITHM HS512 KEY '{secret}'").as_str(),
|
||||
format!("DEFINE ACCESS token ON NS TYPE JWT ALGORITHM HS512 KEY '{secret}' DURATION FOR SESSION 30d").as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
|
@ -790,7 +789,15 @@ mod tests {
|
|||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -815,7 +822,15 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -869,7 +884,7 @@ mod tests {
|
|||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!("DEFINE ACCESS token ON DATABASE TYPE JWT ALGORITHM HS512 KEY '{secret}'")
|
||||
format!("DEFINE ACCESS token ON DATABASE TYPE JWT ALGORITHM HS512 KEY '{secret}' DURATION FOR SESSION 30d")
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
|
@ -900,7 +915,15 @@ mod tests {
|
|||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -926,7 +949,15 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
|
||||
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -984,7 +1015,8 @@ mod tests {
|
|||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS token ON DATABASE TYPE RECORD
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}';
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 30d;
|
||||
|
||||
CREATE user:test;
|
||||
"#
|
||||
|
@ -1021,7 +1053,15 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -1049,7 +1089,15 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -1246,7 +1294,8 @@ mod tests {
|
|||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS token ON DATABASE TYPE RECORD
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}';
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 30d;
|
||||
|
||||
CREATE user:test;
|
||||
"#
|
||||
|
@ -1315,7 +1364,15 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
// Session expiration has been set explicitly
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::days(30) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::days(30) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to match the defined duration"
|
||||
);
|
||||
let tk = match sess.tk {
|
||||
Some(Value::Object(tk)) => tk,
|
||||
_ => panic!("Session token is not an object"),
|
||||
|
@ -1465,7 +1522,7 @@ mod tests {
|
|||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
|
||||
assert_eq!(sess.exp, None, "Default session expiration is expected to be None");
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -1557,4 +1614,43 @@ mod tests {
|
|||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_expired_token() {
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
// Token was issued two hours ago and expired one hour ago
|
||||
iat: Some((Utc::now() - Duration::hours(2)).timestamp()),
|
||||
nbf: Some((Utc::now() - Duration::hours(2)).timestamp()),
|
||||
exp: Some((Utc::now() - Duration::hours(1)).timestamp()),
|
||||
ac: Some("token".to_string()),
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!("DEFINE ACCESS token ON DATABASE TYPE JWT ALGORITHM HS512 KEY '{secret}' DURATION FOR SESSION 30d, FOR TOKEN 30d")
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
assert!(res.is_err(), "Unexpected success signing in with expired token: {:?}", res);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,41 @@
|
|||
use crate::sql::{escape::escape_ident, fmt::Fmt, strand::no_nul_bytes, Id, Ident, Thing};
|
||||
use crate::sql::{
|
||||
escape::escape_ident, fmt::Fmt, strand::no_nul_bytes, Duration, Id, Ident, Thing,
|
||||
};
|
||||
use revision::revisioned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str;
|
||||
|
||||
#[revisioned(revision = 1)]
|
||||
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
// Durations representing the expiration of different elements of the access method
|
||||
// In this context, the None variant represents that the element does not expire
|
||||
pub struct AccessDuration {
|
||||
// Duration after which the grants generated with the access method expire
|
||||
// For access methods whose grants are tokens, this value is irrelevant
|
||||
pub grant: Option<Duration>,
|
||||
// Duration after which the tokens obtained with the access method expire
|
||||
// For access methods that cannot issue tokens, this value is irrelevant
|
||||
pub token: Option<Duration>,
|
||||
// Duration after which the session authenticated with the access method expires
|
||||
pub session: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for AccessDuration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// By default, access grants do not expire
|
||||
grant: None,
|
||||
// By default, tokens expire after one hour
|
||||
token: Some(Duration::from_hours(1)),
|
||||
// By default, sessions do not expire
|
||||
session: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[revisioned(revision = 1)]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::sql::statements::info::InfoStructure;
|
||||
use crate::sql::statements::DefineAccessStatement;
|
||||
use crate::sql::{escape::quote_str, Algorithm, Duration};
|
||||
use crate::sql::{escape::quote_str, Algorithm};
|
||||
use revision::revisioned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
@ -27,6 +27,29 @@ impl Default for AccessType {
|
|||
}
|
||||
}
|
||||
|
||||
impl AccessType {
|
||||
// Returns whether or not the access method can issue non-token grants
|
||||
// In this context, token refers exclusively to JWT
|
||||
#[allow(unreachable_patterns)]
|
||||
pub fn can_issue_grants(&self) -> bool {
|
||||
match self {
|
||||
// The grants for JWT and record access methods are JWT
|
||||
AccessType::Jwt(_) | AccessType::Record(_) => false,
|
||||
// TODO(gguillemas): This arm should be reachable by the bearer access method
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
// Returns whether or not the access method can issue tokens
|
||||
// In this context, tokens refers exclusively to JWT
|
||||
pub fn can_issue_tokens(&self) -> bool {
|
||||
match self {
|
||||
// The JWT access method can only issue tokens if an issuer is set
|
||||
AccessType::Jwt(jwt) => jwt.issue.is_some(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[revisioned(revision = 1)]
|
||||
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
|
@ -52,8 +75,6 @@ impl Default for JwtAccess {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg,
|
||||
key,
|
||||
// Defaults to tokens lasting for one hour
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +111,6 @@ impl JwtAccess {
|
|||
pub struct JwtAccessIssue {
|
||||
pub alg: Algorithm,
|
||||
pub key: String,
|
||||
pub duration: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for JwtAccessIssue {
|
||||
|
@ -100,8 +120,6 @@ impl Default for JwtAccessIssue {
|
|||
alg: Algorithm::Hs512,
|
||||
// Avoid defaulting to empty key
|
||||
key: DefineAccessStatement::random_key(),
|
||||
// Defaults to tokens lasting for one hour
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +171,6 @@ pub struct JwtAccessVerifyJwks {
|
|||
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
pub struct RecordAccess {
|
||||
pub duration: Option<Duration>,
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
|
@ -162,8 +179,6 @@ pub struct RecordAccess {
|
|||
impl Default for RecordAccess {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Defaults to sessions lasting one hour
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -187,9 +202,6 @@ impl Display for AccessType {
|
|||
}
|
||||
AccessType::Record(ac) => {
|
||||
f.write_str(" RECORD")?;
|
||||
if let Some(ref v) = ac.duration {
|
||||
write!(f, " DURATION {v}")?
|
||||
}
|
||||
if let Some(ref v) = ac.signup {
|
||||
write!(f, " SIGNUP {v}")?
|
||||
}
|
||||
|
@ -219,9 +231,6 @@ impl InfoStructure for AccessType {
|
|||
if let Some(signin) = ac.signin {
|
||||
acc.insert("signin".to_string(), signin.structure());
|
||||
}
|
||||
if let Some(duration) = ac.duration {
|
||||
acc.insert("duration".to_string(), duration.into());
|
||||
}
|
||||
acc.insert("jwt".to_string(), ac.jwt.structure());
|
||||
}
|
||||
};
|
||||
|
@ -242,9 +251,6 @@ impl Display for JwtAccess {
|
|||
}
|
||||
if let Some(iss) = &self.issue {
|
||||
write!(f, " WITH ISSUER KEY {}", quote_str(&iss.key))?;
|
||||
if let Some(ref v) = iss.duration {
|
||||
write!(f, " DURATION {v}")?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -266,9 +272,6 @@ impl InfoStructure for JwtAccess {
|
|||
let mut iss = Object::default();
|
||||
iss.insert("alg".to_string(), v.alg.structure());
|
||||
iss.insert("key".to_string(), v.key.into());
|
||||
if let Some(t) = v.duration {
|
||||
iss.insert("duration".to_string(), t.into());
|
||||
}
|
||||
acc.insert("issuer".to_string(), iss.to_string().into());
|
||||
}
|
||||
Value::Object(acc)
|
||||
|
|
|
@ -64,6 +64,7 @@ pub(crate) mod table_type;
|
|||
pub(crate) mod thing;
|
||||
pub(crate) mod timeout;
|
||||
pub(crate) mod tokenizer;
|
||||
pub(crate) mod user;
|
||||
pub(crate) mod uuid;
|
||||
pub(crate) mod value;
|
||||
pub(crate) mod version;
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::doc::CursorDoc;
|
|||
use crate::err::Error;
|
||||
use crate::iam::{Action, ResourceKind};
|
||||
use crate::sql::statements::info::InfoStructure;
|
||||
use crate::sql::{AccessType, Base, Ident, Object, Strand, Value};
|
||||
use crate::sql::{access::AccessDuration, AccessType, Base, Ident, Object, Strand, Value};
|
||||
use derive::Store;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::Rng;
|
||||
|
@ -12,7 +12,7 @@ use revision::revisioned;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[revisioned(revision = 2)]
|
||||
#[revisioned(revision = 1)]
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[non_exhaustive]
|
||||
|
@ -20,8 +20,8 @@ pub struct DefineAccessStatement {
|
|||
pub name: Ident,
|
||||
pub base: Base,
|
||||
pub kind: AccessType,
|
||||
pub duration: AccessDuration,
|
||||
pub comment: Option<Strand>,
|
||||
#[revision(start = 2)]
|
||||
pub if_not_exists: bool,
|
||||
}
|
||||
|
||||
|
@ -132,9 +132,6 @@ impl Display for DefineAccessStatement {
|
|||
}
|
||||
AccessType::Record(ac) => {
|
||||
write!(f, " TYPE RECORD")?;
|
||||
if let Some(ref v) = ac.duration {
|
||||
write!(f, " DURATION {v}")?
|
||||
}
|
||||
if let Some(ref v) = ac.signup {
|
||||
write!(f, " SIGNUP {v}")?
|
||||
}
|
||||
|
@ -144,6 +141,38 @@ impl Display for DefineAccessStatement {
|
|||
write!(f, " WITH JWT {}", ac.jwt)?;
|
||||
}
|
||||
}
|
||||
// Always print relevant durations so defaults can be changed in the future
|
||||
// If default values were not printed, exports would not be forward compatible
|
||||
// None values need to be printed, as they are different from the default values
|
||||
write!(f, " DURATION")?;
|
||||
if self.kind.can_issue_grants() {
|
||||
write!(
|
||||
f,
|
||||
" FOR GRANT {},",
|
||||
match self.duration.grant {
|
||||
Some(dur) => format!("{}", dur),
|
||||
None => "NONE".to_string(),
|
||||
}
|
||||
)?;
|
||||
}
|
||||
if self.kind.can_issue_tokens() {
|
||||
write!(
|
||||
f,
|
||||
" FOR TOKEN {},",
|
||||
match self.duration.token {
|
||||
Some(dur) => format!("{}", dur),
|
||||
None => "NONE".to_string(),
|
||||
}
|
||||
)?;
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
" FOR SESSION {}",
|
||||
match self.duration.session {
|
||||
Some(dur) => format!("{}", dur),
|
||||
None => "NONE".to_string(),
|
||||
}
|
||||
)?;
|
||||
if let Some(ref v) = self.comment {
|
||||
write!(f, " COMMENT {v}")?
|
||||
}
|
||||
|
@ -157,6 +186,7 @@ impl InfoStructure for DefineAccessStatement {
|
|||
name,
|
||||
base,
|
||||
kind,
|
||||
duration,
|
||||
comment,
|
||||
..
|
||||
} = self;
|
||||
|
@ -166,6 +196,16 @@ impl InfoStructure for DefineAccessStatement {
|
|||
|
||||
acc.insert("base".to_string(), base.structure());
|
||||
|
||||
let mut dur = Object::default();
|
||||
if kind.can_issue_grants() {
|
||||
dur.insert("grant".to_string(), duration.grant.into());
|
||||
}
|
||||
if kind.can_issue_tokens() {
|
||||
dur.insert("token".to_string(), duration.token.into());
|
||||
}
|
||||
dur.insert("session".to_string(), duration.session.into());
|
||||
acc.insert("duration".to_string(), dur.to_string().into());
|
||||
|
||||
acc.insert("kind".to_string(), kind.structure());
|
||||
|
||||
if let Some(comment) = comment {
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::doc::CursorDoc;
|
|||
use crate::err::Error;
|
||||
use crate::iam::{Action, ResourceKind};
|
||||
use crate::sql::statements::info::InfoStructure;
|
||||
use crate::sql::{escape::quote_str, fmt::Fmt, Base, Duration, Ident, Object, Strand, Value};
|
||||
use crate::sql::{
|
||||
escape::quote_str, fmt::Fmt, user::UserDuration, Base, Duration, Ident, Object, Strand, Value,
|
||||
};
|
||||
use argon2::{
|
||||
password_hash::{PasswordHasher, SaltString},
|
||||
Argon2,
|
||||
|
@ -26,7 +28,7 @@ pub struct DefineUserStatement {
|
|||
pub code: String,
|
||||
pub roles: Vec<Ident>,
|
||||
#[revision(start = 3)]
|
||||
pub session: Option<Duration>,
|
||||
pub duration: UserDuration,
|
||||
pub comment: Option<Strand>,
|
||||
#[revision(start = 2)]
|
||||
pub if_not_exists: bool,
|
||||
|
@ -47,7 +49,7 @@ impl From<(Base, &str, &str, &str)> for DefineUserStatement {
|
|||
.map(char::from)
|
||||
.collect::<String>(),
|
||||
roles: vec![role.into()],
|
||||
session: None,
|
||||
duration: UserDuration::default(),
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
}
|
||||
|
@ -59,13 +61,13 @@ impl DefineUserStatement {
|
|||
name: Ident,
|
||||
base: Base,
|
||||
roles: Vec<Ident>,
|
||||
session: Option<Duration>,
|
||||
duration: UserDuration,
|
||||
) -> Self {
|
||||
DefineUserStatement {
|
||||
name,
|
||||
base,
|
||||
roles, // New users get the viewer role by default
|
||||
session, // Sessions for system users do not expire by default
|
||||
roles,
|
||||
duration,
|
||||
code: rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(128)
|
||||
|
@ -86,8 +88,12 @@ impl DefineUserStatement {
|
|||
self.hash = passhash;
|
||||
}
|
||||
|
||||
pub(crate) fn set_session(&mut self, session: Option<Duration>) {
|
||||
self.session = session;
|
||||
pub(crate) fn set_token_duration(&mut self, duration: Option<Duration>) {
|
||||
self.duration.token = duration;
|
||||
}
|
||||
|
||||
pub(crate) fn set_session_duration(&mut self, duration: Option<Duration>) {
|
||||
self.duration.session = duration;
|
||||
}
|
||||
|
||||
/// Process this type returning a computed simple Value
|
||||
|
@ -206,9 +212,26 @@ impl Display for DefineUserStatement {
|
|||
&self.roles.iter().map(|r| r.to_string().to_uppercase()).collect::<Vec<String>>()
|
||||
),
|
||||
)?;
|
||||
if let Some(ref v) = self.session {
|
||||
write!(f, " SESSION {v}")?
|
||||
}
|
||||
// Always print relevant durations so defaults can be changed in the future
|
||||
// If default values were not printed, exports would not be forward compatible
|
||||
// None values need to be printed, as they are different from the default values
|
||||
write!(f, " DURATION")?;
|
||||
write!(
|
||||
f,
|
||||
" FOR TOKEN {},",
|
||||
match self.duration.token {
|
||||
Some(dur) => format!("{}", dur),
|
||||
None => "NONE".to_string(),
|
||||
}
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
" FOR SESSION {}",
|
||||
match self.duration.session {
|
||||
Some(dur) => format!("{}", dur),
|
||||
None => "NONE".to_string(),
|
||||
}
|
||||
)?;
|
||||
if let Some(ref v) = self.comment {
|
||||
write!(f, " COMMENT {v}")?
|
||||
}
|
||||
|
@ -223,7 +246,7 @@ impl InfoStructure for DefineUserStatement {
|
|||
base,
|
||||
hash,
|
||||
roles,
|
||||
session,
|
||||
duration,
|
||||
comment,
|
||||
..
|
||||
} = self;
|
||||
|
@ -240,9 +263,10 @@ impl InfoStructure for DefineUserStatement {
|
|||
Value::Array(roles.into_iter().map(|r| r.structure()).collect()),
|
||||
);
|
||||
|
||||
if let Some(session) = session {
|
||||
acc.insert("session".to_string(), session.into());
|
||||
}
|
||||
let mut dur = Object::default();
|
||||
dur.insert("token".to_string(), duration.token.into());
|
||||
dur.insert("session".to_string(), duration.session.into());
|
||||
acc.insert("duration".to_string(), dur.to_string().into());
|
||||
|
||||
if let Some(comment) = comment {
|
||||
acc.insert("comment".to_string(), comment.into());
|
||||
|
|
27
core/src/sql/user.rs
Normal file
27
core/src/sql/user.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use crate::sql::Duration;
|
||||
use revision::revisioned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str;
|
||||
|
||||
#[revisioned(revision = 1)]
|
||||
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
// Durations representing the expiration of different elements of user authentication
|
||||
// In this context, the None variant represents that the element does not expire
|
||||
pub struct UserDuration {
|
||||
// Duration after which the token obtained after authenticating with user credentials expires
|
||||
pub token: Option<Duration>,
|
||||
// Duration after which the session authenticated with user credentials or token expires
|
||||
pub session: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for UserDuration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// By default, tokens expire after one hour
|
||||
token: Some(Duration::from_hours(1)),
|
||||
// By default, sessions do not expire
|
||||
session: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ use crate::sql::access_type::{
|
|||
};
|
||||
use crate::sql::value::serde::ser;
|
||||
use crate::sql::Algorithm;
|
||||
use crate::sql::Duration;
|
||||
use crate::sql::Value;
|
||||
use ser::Serializer as _;
|
||||
use serde::ser::Error as _;
|
||||
|
@ -82,7 +81,6 @@ impl ser::Serializer for SerializerRecord {
|
|||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct SerializeRecord {
|
||||
pub duration: Option<Duration>,
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
|
@ -97,10 +95,6 @@ impl serde::ser::SerializeStruct for SerializeRecord {
|
|||
T: ?Sized + Serialize,
|
||||
{
|
||||
match key {
|
||||
"duration" => {
|
||||
self.duration =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
"signup" => {
|
||||
self.signup = value.serialize(ser::value::opt::Serializer.wrap())?;
|
||||
}
|
||||
|
@ -119,7 +113,6 @@ impl serde::ser::SerializeStruct for SerializeRecord {
|
|||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(RecordAccess {
|
||||
duration: self.duration,
|
||||
signup: self.signup,
|
||||
signin: self.signin,
|
||||
jwt: self.jwt,
|
||||
|
@ -420,7 +413,6 @@ impl ser::Serializer for SerializerJwtIssue {
|
|||
pub struct SerializeJwtIssue {
|
||||
pub alg: Algorithm,
|
||||
pub key: String,
|
||||
pub duration: Option<Duration>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeJwtIssue {
|
||||
|
@ -438,10 +430,6 @@ impl serde::ser::SerializeStruct for SerializeJwtIssue {
|
|||
"key" => {
|
||||
self.key = value.serialize(ser::string::Serializer.wrap())?;
|
||||
}
|
||||
"duration" => {
|
||||
self.duration =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
key => {
|
||||
return Err(Error::custom(format!("unexpected field `JwtAccessIssue::{key}`")));
|
||||
}
|
||||
|
@ -451,7 +439,6 @@ impl serde::ser::SerializeStruct for SerializeJwtIssue {
|
|||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(JwtAccessIssue {
|
||||
duration: self.duration,
|
||||
alg: self.alg,
|
||||
key: self.key,
|
||||
})
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use crate::err::Error;
|
||||
use crate::sql::access::AccessDuration;
|
||||
use crate::sql::access_type::AccessType;
|
||||
use crate::sql::statements::DefineAccessStatement;
|
||||
use crate::sql::value::serde::ser;
|
||||
use crate::sql::Base;
|
||||
use crate::sql::Duration;
|
||||
use crate::sql::Ident;
|
||||
use crate::sql::Strand;
|
||||
use ser::Serializer as _;
|
||||
|
@ -43,6 +45,7 @@ pub struct SerializeDefineAccessStatement {
|
|||
name: Ident,
|
||||
base: Base,
|
||||
kind: AccessType,
|
||||
duration: AccessDuration,
|
||||
comment: Option<Strand>,
|
||||
if_not_exists: bool,
|
||||
}
|
||||
|
@ -65,6 +68,9 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
|
|||
"kind" => {
|
||||
self.kind = value.serialize(ser::access_type::Serializer.wrap())?;
|
||||
}
|
||||
"duration" => {
|
||||
self.duration = value.serialize(SerializerDuration.wrap())?;
|
||||
}
|
||||
"comment" => {
|
||||
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
|
||||
}
|
||||
|
@ -85,12 +91,84 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
|
|||
name: self.name,
|
||||
base: self.base,
|
||||
kind: self.kind,
|
||||
duration: self.duration,
|
||||
comment: self.comment,
|
||||
if_not_exists: self.if_not_exists,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerializerDuration;
|
||||
|
||||
impl ser::Serializer for SerializerDuration {
|
||||
type Ok = AccessDuration;
|
||||
type Error = Error;
|
||||
|
||||
type SerializeSeq = Impossible<AccessDuration, Error>;
|
||||
type SerializeTuple = Impossible<AccessDuration, Error>;
|
||||
type SerializeTupleStruct = Impossible<AccessDuration, Error>;
|
||||
type SerializeTupleVariant = Impossible<AccessDuration, Error>;
|
||||
type SerializeMap = Impossible<AccessDuration, Error>;
|
||||
type SerializeStruct = SerializeDuration;
|
||||
type SerializeStructVariant = Impossible<AccessDuration, Error>;
|
||||
|
||||
const EXPECTED: &'static str = "a struct `AccessDuration`";
|
||||
|
||||
#[inline]
|
||||
fn serialize_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStruct, Error> {
|
||||
Ok(SerializeDuration::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct SerializeDuration {
|
||||
pub grant: Option<Duration>,
|
||||
pub token: Option<Duration>,
|
||||
pub session: Option<Duration>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeDuration {
|
||||
type Ok = AccessDuration;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
match key {
|
||||
"grant" => {
|
||||
self.grant =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
"token" => {
|
||||
self.token =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
"session" => {
|
||||
self.session =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
key => {
|
||||
return Err(Error::custom(format!("unexpected field `AccessDuration::{key}`")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(AccessDuration {
|
||||
grant: self.grant,
|
||||
token: self.token,
|
||||
session: self.session,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::err::Error;
|
||||
use crate::sql::statements::DefineUserStatement;
|
||||
use crate::sql::user::UserDuration;
|
||||
use crate::sql::value::serde::ser;
|
||||
use crate::sql::Base;
|
||||
use crate::sql::Duration;
|
||||
|
@ -45,7 +46,7 @@ pub struct SerializeDefineUserStatement {
|
|||
hash: String,
|
||||
code: String,
|
||||
roles: Vec<Ident>,
|
||||
session: Option<Duration>,
|
||||
duration: UserDuration,
|
||||
comment: Option<Strand>,
|
||||
if_not_exists: bool,
|
||||
}
|
||||
|
@ -74,9 +75,8 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
|
|||
"roles" => {
|
||||
self.roles = value.serialize(ser::ident::vec::Serializer.wrap())?;
|
||||
}
|
||||
"session" => {
|
||||
self.session =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
"duration" => {
|
||||
self.duration = value.serialize(SerializerDuration.wrap())?;
|
||||
}
|
||||
"comment" => {
|
||||
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
|
||||
|
@ -100,12 +100,76 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
|
|||
hash: self.hash,
|
||||
code: self.code,
|
||||
roles: self.roles,
|
||||
session: self.session,
|
||||
duration: self.duration,
|
||||
comment: self.comment,
|
||||
if_not_exists: self.if_not_exists,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub struct SerializerDuration;
|
||||
|
||||
impl ser::Serializer for SerializerDuration {
|
||||
type Ok = UserDuration;
|
||||
type Error = Error;
|
||||
|
||||
type SerializeSeq = Impossible<UserDuration, Error>;
|
||||
type SerializeTuple = Impossible<UserDuration, Error>;
|
||||
type SerializeTupleStruct = Impossible<UserDuration, Error>;
|
||||
type SerializeTupleVariant = Impossible<UserDuration, Error>;
|
||||
type SerializeMap = Impossible<UserDuration, Error>;
|
||||
type SerializeStruct = SerializeDuration;
|
||||
type SerializeStructVariant = Impossible<UserDuration, Error>;
|
||||
|
||||
const EXPECTED: &'static str = "a struct `UserDuration`";
|
||||
|
||||
#[inline]
|
||||
fn serialize_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStruct, Error> {
|
||||
Ok(SerializeDuration::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct SerializeDuration {
|
||||
pub token: Option<Duration>,
|
||||
pub session: Option<Duration>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeDuration {
|
||||
type Ok = UserDuration;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
match key {
|
||||
"token" => {
|
||||
self.token =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
"session" => {
|
||||
self.session =
|
||||
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
|
||||
}
|
||||
key => {
|
||||
return Err(Error::custom(format!("unexpected field `UserDuration::{key}`")));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(UserDuration {
|
||||
token: self.token,
|
||||
session: self.session,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -117,4 +181,17 @@ mod tests {
|
|||
let value: DefineUserStatement = stmt.serialize(Serializer.wrap()).unwrap();
|
||||
assert_eq!(value, stmt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_durations() {
|
||||
let stmt = DefineUserStatement {
|
||||
duration: UserDuration {
|
||||
token: Some(Duration::from_mins(15)),
|
||||
session: Some(Duration::from_mins(90)),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let value: DefineUserStatement = stmt.serialize(Serializer.wrap()).unwrap();
|
||||
assert_eq!(value, stmt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -519,6 +519,15 @@ impl From<Option<i64>> for Value {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Duration>> for Value {
|
||||
fn from(v: Option<Duration>) -> Self {
|
||||
match v {
|
||||
Some(v) => Value::from(v),
|
||||
None => Value::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for Value {
|
||||
fn from(v: Id) -> Self {
|
||||
match v {
|
||||
|
|
|
@ -123,6 +123,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
|
|||
UniCase::ascii("FROM") => TokenKind::Keyword(Keyword::From),
|
||||
UniCase::ascii("FULL") => TokenKind::Keyword(Keyword::Full),
|
||||
UniCase::ascii("FUNCTION") => TokenKind::Keyword(Keyword::Function),
|
||||
UniCase::ascii("GRANT") => TokenKind::Keyword(Keyword::Grant),
|
||||
UniCase::ascii("GROUP") => TokenKind::Keyword(Keyword::Group),
|
||||
UniCase::ascii("HIGHLIGHTS") => TokenKind::Keyword(Keyword::Highlights),
|
||||
UniCase::ascii("HNSW") => TokenKind::Keyword(Keyword::Hnsw),
|
||||
|
|
|
@ -16,8 +16,8 @@ use crate::{
|
|||
},
|
||||
table_type,
|
||||
tokenizer::Tokenizer,
|
||||
AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType,
|
||||
Values,
|
||||
user, AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand,
|
||||
TableType, Values,
|
||||
},
|
||||
syn::{
|
||||
parser::{
|
||||
|
@ -182,7 +182,7 @@ impl Parser<'_> {
|
|||
name,
|
||||
base,
|
||||
vec!["Viewer".into()], // New users get the viewer role by default
|
||||
None, // Sessions for system users do not expire by default
|
||||
user::UserDuration::default(),
|
||||
);
|
||||
|
||||
if if_not_exists {
|
||||
|
@ -210,9 +210,35 @@ impl Parser<'_> {
|
|||
res.roles.push(self.next_token_value()?);
|
||||
}
|
||||
}
|
||||
t!("SESSION") => {
|
||||
t!("DURATION") => {
|
||||
self.pop_peek();
|
||||
res.set_session(Some(self.next_token_value()?));
|
||||
while self.eat(t!("FOR")) {
|
||||
match self.peek_kind() {
|
||||
t!("TOKEN") => {
|
||||
self.pop_peek();
|
||||
match self.peek_kind() {
|
||||
t!("NONE") => {
|
||||
// Currently, SurrealDB does not accept tokens without expiration.
|
||||
// For this reason, some token duration must be set.
|
||||
unexpected!(self, t!("NONE"), "a token duration");
|
||||
}
|
||||
_ => res.set_token_duration(Some(self.next_token_value()?)),
|
||||
}
|
||||
}
|
||||
t!("SESSION") => {
|
||||
self.pop_peek();
|
||||
match self.peek_kind() {
|
||||
t!("NONE") => {
|
||||
self.pop_peek();
|
||||
res.set_session_duration(None)
|
||||
}
|
||||
_ => res.set_session_duration(Some(self.next_token_value()?)),
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
self.eat(t!(","));
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
|
@ -255,7 +281,7 @@ impl Parser<'_> {
|
|||
match self.peek_kind() {
|
||||
t!("JWT") => {
|
||||
self.pop_peek();
|
||||
res.kind = AccessType::Jwt(self.parse_jwt(None)?);
|
||||
res.kind = AccessType::Jwt(self.parse_jwt()?);
|
||||
}
|
||||
t!("RECORD") => {
|
||||
self.pop_peek();
|
||||
|
@ -264,15 +290,6 @@ impl Parser<'_> {
|
|||
};
|
||||
loop {
|
||||
match self.peek_kind() {
|
||||
t!("DURATION") => {
|
||||
self.pop_peek();
|
||||
ac.duration = Some(self.next_token_value()?);
|
||||
// By default, token duration matches session duration
|
||||
// The token duration can be modified in the WITH JWT clause
|
||||
if let Some(ref mut iss) = ac.jwt.issue {
|
||||
iss.duration = ac.duration;
|
||||
}
|
||||
}
|
||||
t!("SIGNUP") => {
|
||||
self.pop_peek();
|
||||
ac.signup =
|
||||
|
@ -288,13 +305,55 @@ impl Parser<'_> {
|
|||
}
|
||||
if self.eat(t!("WITH")) {
|
||||
expected!(self, t!("JWT"));
|
||||
ac.jwt = self.parse_jwt(Some(AccessType::Record(ac.clone())))?;
|
||||
ac.jwt = self.parse_jwt()?;
|
||||
}
|
||||
res.kind = AccessType::Record(ac);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
t!("DURATION") => {
|
||||
self.pop_peek();
|
||||
while self.eat(t!("FOR")) {
|
||||
match self.peek_kind() {
|
||||
t!("GRANT") => {
|
||||
self.pop_peek();
|
||||
match self.peek_kind() {
|
||||
t!("NONE") => {
|
||||
self.pop_peek();
|
||||
res.duration.grant = None
|
||||
}
|
||||
_ => res.duration.grant = Some(self.next_token_value()?),
|
||||
}
|
||||
}
|
||||
t!("TOKEN") => {
|
||||
self.pop_peek();
|
||||
match self.peek_kind() {
|
||||
t!("NONE") => {
|
||||
// Currently, SurrealDB does not accept tokens without expiration.
|
||||
// For this reason, some token duration must be set.
|
||||
// In the future, allowing issuing tokens without expiration may be useful.
|
||||
// Tokens issued by access methods can be consumed by third parties that support it.
|
||||
unexpected!(self, t!("NONE"), "a token duration");
|
||||
}
|
||||
_ => res.duration.token = Some(self.next_token_value()?),
|
||||
}
|
||||
}
|
||||
t!("SESSION") => {
|
||||
self.pop_peek();
|
||||
match self.peek_kind() {
|
||||
t!("NONE") => {
|
||||
self.pop_peek();
|
||||
res.duration.session = None
|
||||
}
|
||||
_ => res.duration.session = Some(self.next_token_value()?),
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
self.eat(t!(","));
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
@ -444,11 +503,7 @@ impl Parser<'_> {
|
|||
}
|
||||
t!("SESSION") => {
|
||||
self.pop_peek();
|
||||
ac.duration = Some(self.next_token_value()?);
|
||||
// By default, token duration matches session duration.
|
||||
if let Some(ref mut iss) = ac.jwt.issue {
|
||||
iss.duration = ac.duration;
|
||||
}
|
||||
res.duration.session = Some(self.next_token_value()?);
|
||||
}
|
||||
t!("SIGNUP") => {
|
||||
self.pop_peek();
|
||||
|
@ -1083,7 +1138,7 @@ impl Parser<'_> {
|
|||
Ok(Kind::Record(names))
|
||||
}
|
||||
|
||||
pub fn parse_jwt(&mut self, ac: Option<AccessType>) -> ParseResult<access_type::JwtAccess> {
|
||||
pub fn parse_jwt(&mut self) -> ParseResult<access_type::JwtAccess> {
|
||||
let mut res = access_type::JwtAccess {
|
||||
// By default, a JWT access method is only used to verify.
|
||||
issue: None,
|
||||
|
@ -1094,13 +1149,6 @@ impl Parser<'_> {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
// If an access method was passed, inherit any relevant defaults.
|
||||
// This will become a match statement whenever more access methods are available.
|
||||
if let Some(AccessType::Record(ac)) = ac {
|
||||
// By default, token duration is inherited from session duration in record access.
|
||||
iss.duration = ac.duration;
|
||||
}
|
||||
|
||||
match self.peek_kind() {
|
||||
t!("ALGORITHM") => {
|
||||
self.pop_peek();
|
||||
|
@ -1176,10 +1224,6 @@ impl Parser<'_> {
|
|||
}
|
||||
iss.key = key;
|
||||
}
|
||||
t!("DURATION") => {
|
||||
self.pop_peek();
|
||||
iss.duration = Some(self.next_token_value()?);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
sql::{
|
||||
access::AccessDuration,
|
||||
access_type::{
|
||||
AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks,
|
||||
JwtAccessVerifyKey, RecordAccess,
|
||||
|
@ -10,7 +11,9 @@ use crate::{
|
|||
index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType},
|
||||
language::Language,
|
||||
statements::{
|
||||
analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement,
|
||||
analyze::AnalyzeStatement,
|
||||
show::{ShowSince, ShowStatement},
|
||||
sleep::SleepStatement,
|
||||
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement,
|
||||
CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
|
||||
DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
|
||||
|
@ -25,6 +28,7 @@ use crate::{
|
|||
UseStatement,
|
||||
},
|
||||
tokenizer::Tokenizer,
|
||||
user::UserDuration,
|
||||
Algorithm, Array, Base, Block, Cond, Data, Datetime, Dir, Duration, Edges, Explain,
|
||||
Expression, Fetch, Fetchs, Field, Fields, Future, Graph, Group, Groups, Id, Ident, Idiom,
|
||||
Idioms, Index, Kind, Limit, Number, Object, Operator, Order, Orders, Output, Param, Part,
|
||||
|
@ -206,21 +210,140 @@ fn parse_define_function() {
|
|||
|
||||
#[test]
|
||||
fn parse_define_user() {
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' ROLES foo, bar COMMENT "*******""#
|
||||
)
|
||||
.unwrap();
|
||||
// Password.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSWORD 'hunter2' COMMENT "*******""#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert_eq!(stmt.hash, "hunter2".to_owned());
|
||||
assert_eq!(stmt.roles, vec![Ident("foo".to_string()), Ident("bar".to_string())]);
|
||||
assert_eq!(stmt.comment, Some(Strand("*******".to_string())))
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert!(stmt.hash.starts_with("$argon2id$"));
|
||||
assert_eq!(stmt.roles, vec![Ident("Viewer".to_string())]);
|
||||
assert_eq!(stmt.comment, Some(Strand("*******".to_string())));
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
UserDuration {
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
// Passhash.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' COMMENT "*******""#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert_eq!(stmt.hash, "hunter2".to_owned());
|
||||
assert_eq!(stmt.roles, vec![Ident("Viewer".to_string())]);
|
||||
assert_eq!(stmt.comment, Some(Strand("*******".to_string())));
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
UserDuration {
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
// With roles.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' ROLES foo, bar"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert_eq!(stmt.hash, "hunter2".to_owned());
|
||||
assert_eq!(stmt.roles, vec![Ident("foo".to_string()), Ident("bar".to_string())]);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
UserDuration {
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
// With session duration.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' DURATION FOR SESSION 6h"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert_eq!(stmt.hash, "hunter2".to_owned());
|
||||
assert_eq!(stmt.roles, vec![Ident("Viewer".to_string())]);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
UserDuration {
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: Some(Duration::from_hours(6)),
|
||||
}
|
||||
);
|
||||
}
|
||||
// With session and token duration.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' DURATION FOR TOKEN 15m, FOR SESSION 6h"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Statement::Define(DefineStatement::User(stmt)) = res else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
assert_eq!(stmt.name, Ident("user".to_string()));
|
||||
assert_eq!(stmt.base, Base::Root);
|
||||
assert_eq!(stmt.hash, "hunter2".to_owned());
|
||||
assert_eq!(stmt.roles, vec![Ident("Viewer".to_string())]);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
UserDuration {
|
||||
token: Some(Duration::from_mins(15)),
|
||||
session: Some(Duration::from_hours(6)),
|
||||
}
|
||||
);
|
||||
}
|
||||
// With none token duration.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' DURATION FOR TOKEN NONE"#
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Unexpected successful parsing of user with none token duration: {:?}",
|
||||
res
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
|
||||
|
@ -243,6 +366,12 @@ fn parse_define_token() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -266,12 +395,19 @@ fn parse_define_token_on_scope() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db); // Scope base is ignored.
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
// Default durations.
|
||||
AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
|
||||
assert_eq!(stmt.if_not_exists, false);
|
||||
match stmt.kind {
|
||||
AccessType::Record(ac) => {
|
||||
// A session duration of one hour is set by default.
|
||||
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
|
||||
assert_eq!(ac.signup, None);
|
||||
assert_eq!(ac.signin, None);
|
||||
match ac.jwt.verify {
|
||||
|
@ -305,6 +441,12 @@ fn parse_define_token_jwks() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -328,12 +470,19 @@ fn parse_define_token_jwks_on_scope() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db); // Scope base is ignored.
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
// Default durations.
|
||||
AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
|
||||
assert_eq!(stmt.if_not_exists, false);
|
||||
match stmt.kind {
|
||||
AccessType::Record(ac) => {
|
||||
// A session duration of one hour is set by default.
|
||||
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
|
||||
assert_eq!(ac.signup, None);
|
||||
assert_eq!(ac.signin, None);
|
||||
match ac.jwt.verify {
|
||||
|
@ -366,10 +515,17 @@ fn parse_define_scope() {
|
|||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db);
|
||||
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: Some(Duration::from_secs(1)),
|
||||
}
|
||||
);
|
||||
assert_eq!(stmt.if_not_exists, false);
|
||||
match stmt.kind {
|
||||
AccessType::Record(ac) => {
|
||||
assert_eq!(ac.duration, Some(Duration(std::time::Duration::from_secs(1))));
|
||||
assert_eq!(ac.signup, Some(Value::Bool(true)));
|
||||
assert_eq!(ac.signin, Some(Value::Bool(false)));
|
||||
match ac.jwt.verify {
|
||||
|
@ -381,8 +537,6 @@ fn parse_define_scope() {
|
|||
match ac.jwt.issue {
|
||||
Some(iss) => {
|
||||
assert_eq!(iss.alg, Algorithm::Hs512);
|
||||
// Token duration matches session duration by default.
|
||||
assert_eq!(iss.duration, Some(Duration::from_secs(1)));
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
|
@ -412,6 +566,12 @@ fn parse_define_access_jwt_key() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -437,10 +597,14 @@ fn parse_define_access_jwt_key() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::EdDSA,
|
||||
key: "bar".to_string(),
|
||||
// Default duration.
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}),
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -466,20 +630,24 @@ fn parse_define_access_jwt_key() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Hs256,
|
||||
key: "foo".to_string(),
|
||||
// Default duration.
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}),
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
)
|
||||
}
|
||||
// Symmetric verify and explicit issue.
|
||||
// Symmetric verify and explicit duration.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" DURATION FOR TOKEN 10s"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -495,9 +663,13 @@ fn parse_define_access_jwt_key() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Hs256,
|
||||
key: "foo".to_string(),
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -507,7 +679,7 @@ fn parse_define_access_jwt_key() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS256 KEY "foo" DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS256 KEY "foo""#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -523,9 +695,14 @@ fn parse_define_access_jwt_key() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Hs256,
|
||||
key: "foo".to_string(),
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -535,7 +712,7 @@ fn parse_define_access_jwt_key() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 KEY "bar" DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 KEY "bar" DURATION FOR TOKEN 10s"#
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
|
@ -547,7 +724,7 @@ fn parse_define_access_jwt_key() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER KEY "bar" DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER KEY "bar" DURATION FOR TOKEN 10s"#
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
|
@ -559,7 +736,7 @@ fn parse_define_access_jwt_key() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 DURATION FOR TOKEN 10s"#
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
|
@ -567,6 +744,18 @@ fn parse_define_access_jwt_key() {
|
|||
res
|
||||
);
|
||||
}
|
||||
// Symmetric verify and token duration is none.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" DURATION FOR TOKEN NONE"#
|
||||
);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Unexpected successful parsing of JWT access with none token duration: {:?}",
|
||||
res
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -589,6 +778,12 @@ fn parse_define_access_jwt_jwks() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -613,10 +808,14 @@ fn parse_define_access_jwt_jwks() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Hs384,
|
||||
key: "foo".to_string(),
|
||||
// Default duration.
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}),
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -626,7 +825,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM HS384 KEY "foo" DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM HS384 KEY "foo" DURATION FOR TOKEN 10s"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -641,9 +840,13 @@ fn parse_define_access_jwt_jwks() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Hs384,
|
||||
key: "foo".to_string(),
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -668,10 +871,14 @@ fn parse_define_access_jwt_jwks() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Ps256,
|
||||
key: "foo".to_string(),
|
||||
// Default duration.
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
}),
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -681,7 +888,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM PS256 KEY "foo" DURATION 10s"#
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM PS256 KEY "foo" DURATION FOR TOKEN 10s, FOR SESSION 2d"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -696,9 +903,13 @@ fn parse_define_access_jwt_jwks() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Ps256,
|
||||
key: "foo".to_string(),
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: Some(Duration::from_days(2)),
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -721,12 +932,19 @@ fn parse_define_access_record() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
// Default durations.
|
||||
AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
|
||||
assert_eq!(stmt.if_not_exists, false);
|
||||
match stmt.kind {
|
||||
AccessType::Record(ac) => {
|
||||
// A session duration of one hour is set by default.
|
||||
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
|
||||
assert_eq!(ac.signup, None);
|
||||
assert_eq!(ac.signin, None);
|
||||
match ac.jwt.verify {
|
||||
|
@ -738,8 +956,6 @@ fn parse_define_access_record() {
|
|||
match ac.jwt.issue {
|
||||
Some(iss) => {
|
||||
assert_eq!(iss.alg, Algorithm::Hs512);
|
||||
// A token duration of one hour is set by default.
|
||||
assert_eq!(iss.duration, Some(Duration::from_hours(1)));
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
|
@ -747,11 +963,11 @@ fn parse_define_access_record() {
|
|||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
// Duration and signing queries are explicitly defined.
|
||||
// Session duration and signing queries are explicitly defined.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s SIGNUP true SIGNIN false"#
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD SIGNUP true SIGNIN false DURATION FOR SESSION 7d"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -763,11 +979,18 @@ fn parse_define_access_record() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: Some(Duration::from_days(7)),
|
||||
}
|
||||
);
|
||||
assert_eq!(stmt.comment, None);
|
||||
assert_eq!(stmt.if_not_exists, false);
|
||||
match stmt.kind {
|
||||
AccessType::Record(ac) => {
|
||||
assert_eq!(ac.duration, Some(Duration(std::time::Duration::from_secs(10))));
|
||||
assert_eq!(ac.signup, Some(Value::Bool(true)));
|
||||
assert_eq!(ac.signin, Some(Value::Bool(false)));
|
||||
match ac.jwt.verify {
|
||||
|
@ -779,8 +1002,6 @@ fn parse_define_access_record() {
|
|||
match ac.jwt.issue {
|
||||
Some(iss) => {
|
||||
assert_eq!(iss.alg, Algorithm::Hs512);
|
||||
// Token duration matches session duration by default.
|
||||
assert_eq!(iss.duration, Some(Duration::from_secs(10)));
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
|
@ -792,7 +1013,7 @@ fn parse_define_access_record() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM HS384 KEY "foo""#
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD WITH JWT ALGORITHM HS384 KEY "foo" DURATION FOR TOKEN 10s, FOR SESSION 15m"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -801,7 +1022,6 @@ fn parse_define_access_record() {
|
|||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Record(RecordAccess {
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -813,11 +1033,14 @@ fn parse_define_access_record() {
|
|||
alg: Algorithm::Hs384,
|
||||
// Issuer key matches verification key by default in symmetric algorithms.
|
||||
key: "foo".to_string(),
|
||||
// Token duration matches session duration by default.
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: Some(Duration::from_mins(15)),
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -827,7 +1050,7 @@ fn parse_define_access_record() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM PS512 KEY "foo" WITH ISSUER KEY "bar""#
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD WITH JWT ALGORITHM PS512 KEY "foo" WITH ISSUER KEY "bar" DURATION FOR TOKEN 10s, FOR SESSION 15m"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -836,7 +1059,6 @@ fn parse_define_access_record() {
|
|||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Record(RecordAccess {
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -847,11 +1069,14 @@ fn parse_define_access_record() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Ps512,
|
||||
key: "bar".to_string(),
|
||||
// Token duration matches session duration by default.
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: Some(Duration::from_mins(15)),
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
@ -861,7 +1086,7 @@ fn parse_define_access_record() {
|
|||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM RS256 KEY 'foo' WITH ISSUER KEY 'bar' DURATION 1m"#
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD WITH JWT ALGORITHM RS256 KEY 'foo' WITH ISSUER KEY 'bar' DURATION FOR TOKEN 10s, FOR SESSION 15m"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -870,7 +1095,6 @@ fn parse_define_access_record() {
|
|||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Record(RecordAccess {
|
||||
duration: Some(Duration::from_secs(10)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -881,15 +1105,29 @@ fn parse_define_access_record() {
|
|||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::Rs256,
|
||||
key: "bar".to_string(),
|
||||
duration: Some(Duration::from_mins(1)),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
session: Some(Duration::from_mins(15)),
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
// Verification with JWT is explicitly defined only with symmetric key. Token duration is none.
|
||||
{
|
||||
let res =
|
||||
test_parse!(parse_stmt, r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION FOR TOKEN NONE"#);
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Unexpected successful parsing of record access with none token duration: {:?}",
|
||||
res
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_define_access_record_with_jwt() {
|
||||
|
@ -904,7 +1142,6 @@ fn parse_define_access_record_with_jwt() {
|
|||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Record(RecordAccess {
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -915,6 +1152,12 @@ fn parse_define_access_record_with_jwt() {
|
|||
issue: None,
|
||||
}
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
sql::{
|
||||
access::AccessDuration,
|
||||
access_type::{AccessType, JwtAccess, JwtAccessVerify, JwtAccessVerifyKey, RecordAccess},
|
||||
block::Entry,
|
||||
changefeed::ChangeFeed,
|
||||
|
@ -195,7 +196,6 @@ fn statements() -> Vec<Statement> {
|
|||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Record(RecordAccess {
|
||||
duration: Some(Duration::from_hours(1)),
|
||||
signup: None,
|
||||
signin: None,
|
||||
jwt: JwtAccess {
|
||||
|
@ -206,6 +206,12 @@ fn statements() -> Vec<Statement> {
|
|||
issue: None,
|
||||
},
|
||||
}),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: Some(Strand("bar".to_string())),
|
||||
if_not_exists: false,
|
||||
})),
|
||||
|
|
|
@ -84,6 +84,7 @@ keyword! {
|
|||
From => "FROM",
|
||||
Full => "FULL",
|
||||
Function => "FUNCTION",
|
||||
Grant => "GRANT",
|
||||
Group => "GROUP",
|
||||
Highlights => "HIGHLIGHTS",
|
||||
Hnsw => "HNSW",
|
||||
|
|
|
@ -59,9 +59,10 @@ async fn signup_record() {
|
|||
let access = Ulid::new().to_string();
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1d FOR TOKEN 15s
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
|
@ -130,9 +131,10 @@ async fn signin_record() {
|
|||
let pass = "password123";
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1d FOR TOKEN 15s
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
|
@ -172,9 +174,10 @@ async fn record_access_throws_error() {
|
|||
let pass = "password123";
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD
|
||||
SIGNUP {{ THROW 'signup_thrown_error' }}
|
||||
SIGNIN {{ THROW 'signin_thrown_error' }}
|
||||
DURATION FOR SESSION 1d FOR TOKEN 15s
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
|
@ -234,9 +237,10 @@ async fn record_access_invalid_query() {
|
|||
let pass = "password123";
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS `{access}` ON DB TYPE RECORD
|
||||
SIGNUP {{ SELECT * FROM ONLY [1, 2] }}
|
||||
SIGNIN {{ SELECT * FROM ONLY [1, 2] }}
|
||||
DURATION FOR SESSION 1d FOR TOKEN 15s
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
|
|
|
@ -1704,7 +1704,7 @@ async fn permissions_checks_define_access_ns() {
|
|||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h\" }, databases: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, databases: { }, users: { } }"],
|
||||
vec!["{ accesses: { }, databases: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -1746,7 +1746,7 @@ async fn permissions_checks_define_access_db() {
|
|||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -1782,13 +1782,13 @@ async fn permissions_checks_define_access_db() {
|
|||
async fn permissions_checks_define_user_root() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", ""),
|
||||
("test", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("test", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("check", "INFO FOR ROOT"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
|
||||
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"],
|
||||
vec!["{ namespaces: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -1824,13 +1824,13 @@ async fn permissions_checks_define_user_root() {
|
|||
async fn permissions_checks_define_user_ns() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", ""),
|
||||
("test", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("test", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("check", "INFO FOR NS"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
|
||||
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"],
|
||||
vec!["{ accesses: { }, databases: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -1866,13 +1866,13 @@ async fn permissions_checks_define_user_ns() {
|
|||
async fn permissions_checks_define_user_db() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", ""),
|
||||
("test", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("test", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("check", "INFO FOR DB"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -1908,13 +1908,13 @@ async fn permissions_checks_define_user_db() {
|
|||
async fn permissions_checks_define_access_record() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", ""),
|
||||
("test", "DEFINE ACCESS account ON DATABASE TYPE RECORD DURATION 1h WITH JWT ALGORITHM HS512 KEY 'secret'"),
|
||||
("test", "DEFINE ACCESS account ON DATABASE TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 12h"),
|
||||
("check", "INFO FOR DB"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { account: \"DEFINE ACCESS account ON DATABASE TYPE RECORD DURATION 1h WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { account: \"DEFINE ACCESS account ON DATABASE TYPE RECORD WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 15m, FOR SESSION 12h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
|
||||
];
|
||||
|
||||
|
@ -2601,8 +2601,8 @@ async fn redefining_existing_table_with_if_not_exists_should_error() -> Result<(
|
|||
#[tokio::test]
|
||||
async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
|
||||
let sql = "
|
||||
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
|
||||
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
|
||||
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
";
|
||||
let dbs = new_ds().await?;
|
||||
let ses = Session::owner().with_ns("test").with_db("test");
|
||||
|
@ -2621,8 +2621,8 @@ async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
|
|||
#[tokio::test]
|
||||
async fn redefining_existing_user_with_if_not_exists_should_error() -> Result<(), Error> {
|
||||
let sql = "
|
||||
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
|
||||
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
|
||||
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
";
|
||||
let dbs = new_ds().await?;
|
||||
let ses = Session::owner().with_ns("test").with_db("test");
|
||||
|
|
|
@ -69,7 +69,7 @@ async fn info_for_db() {
|
|||
let sql = r#"
|
||||
DEFINE TABLE TB;
|
||||
DEFINE ACCESS jwt ON DB TYPE JWT ALGORITHM HS512 KEY 'secret';
|
||||
DEFINE ACCESS record ON DB TYPE RECORD DURATION 24h;
|
||||
DEFINE ACCESS record ON DB TYPE RECORD DURATION FOR TOKEN 30m, FOR SESSION 12h;
|
||||
DEFINE USER user ON DB PASSWORD 'pass';
|
||||
DEFINE FUNCTION fn::greet() {RETURN "Hello";};
|
||||
DEFINE PARAM $param VALUE "foo";
|
||||
|
@ -363,15 +363,15 @@ async fn permissions_checks_info_table() {
|
|||
#[tokio::test]
|
||||
async fn permissions_checks_info_user_root() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("prepare", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("test", "INFO FOR USER user ON ROOT"),
|
||||
("check", "INFO FOR USER user ON ROOT"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -405,15 +405,15 @@ async fn permissions_checks_info_user_root() {
|
|||
#[tokio::test]
|
||||
async fn permissions_checks_info_user_ns() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("prepare", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("test", "INFO FOR USER user ON NS"),
|
||||
("check", "INFO FOR USER user ON NS"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -447,15 +447,15 @@ async fn permissions_checks_info_user_ns() {
|
|||
#[tokio::test]
|
||||
async fn permissions_checks_info_user_db() {
|
||||
let scenario = HashMap::from([
|
||||
("prepare", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
|
||||
("prepare", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h"),
|
||||
("test", "INFO FOR USER user ON DB"),
|
||||
("check", "INFO FOR USER user ON DB"),
|
||||
]);
|
||||
|
||||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
|
||||
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -504,7 +504,7 @@ async fn access_info_redacted() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h" }, databases: { }, users: { } }"#.to_string();
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE" }, databases: { }, users: { } }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
@ -527,7 +527,7 @@ async fn access_info_redacted() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM PS512 KEY 'public' WITH ISSUER KEY '[REDACTED]' DURATION 1h" }, databases: { }, users: { } }"#.to_string();
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM PS512 KEY 'public' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE" }, databases: { }, users: { } }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
@ -550,7 +550,7 @@ async fn access_info_redacted() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE RECORD DURATION 1h WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h" }, databases: { }, users: { } }"#.to_string();
|
||||
r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE RECORD WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE" }, databases: { }, users: { } }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
@ -564,7 +564,7 @@ async fn access_info_redacted_structure() {
|
|||
// Symmetric
|
||||
{
|
||||
let sql = r#"
|
||||
DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret';
|
||||
DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
INFO FOR NS STRUCTURE
|
||||
"#;
|
||||
let dbs = new_ds().await.unwrap();
|
||||
|
@ -577,7 +577,7 @@ async fn access_info_redacted_structure() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', kind: { jwt: { alg: 'HS512', issuer: "{ alg: 'HS512', duration: 1h, key: '[REDACTED]' }", key: '[REDACTED]' }, kind: 'JWT' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', duration: '{ session: 6h, token: 15m }', kind: { jwt: { alg: 'HS512', issuer: "{ alg: 'HS512', key: '[REDACTED]' }", key: '[REDACTED]' }, kind: 'JWT' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
@ -587,7 +587,7 @@ async fn access_info_redacted_structure() {
|
|||
// Asymmetric
|
||||
{
|
||||
let sql = r#"
|
||||
DEFINE ACCESS access ON NS TYPE JWT ALGORITHM PS512 KEY 'public' WITH ISSUER KEY 'private';
|
||||
DEFINE ACCESS access ON NS TYPE JWT ALGORITHM PS512 KEY 'public' WITH ISSUER KEY 'private' DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
INFO FOR NS STRUCTURE
|
||||
"#;
|
||||
let dbs = new_ds().await.unwrap();
|
||||
|
@ -600,7 +600,7 @@ async fn access_info_redacted_structure() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', kind: { jwt: { alg: 'PS512', issuer: "{ alg: 'PS512', duration: 1h, key: '[REDACTED]' }", key: 'public' }, kind: 'JWT' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', duration: '{ session: 6h, token: 15m }', kind: { jwt: { alg: 'PS512', issuer: "{ alg: 'PS512', key: '[REDACTED]' }", key: 'public' }, kind: 'JWT' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
@ -610,7 +610,7 @@ async fn access_info_redacted_structure() {
|
|||
// Record
|
||||
{
|
||||
let sql = r#"
|
||||
DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret';
|
||||
DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h;
|
||||
INFO FOR NS STRUCTURE
|
||||
"#;
|
||||
let dbs = new_ds().await.unwrap();
|
||||
|
@ -623,7 +623,7 @@ async fn access_info_redacted_structure() {
|
|||
assert!(out.is_ok(), "Unexpected error: {:?}", out);
|
||||
|
||||
let out_expected =
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', kind: { duration: 1h, jwt: { alg: 'HS512', issuer: "{ alg: 'HS512', duration: 1h, key: '[REDACTED]' }", key: '[REDACTED]' }, kind: 'RECORD' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
r#"{ accesses: [{ base: 'NAMESPACE', duration: '{ session: 6h, token: 15m }', kind: { jwt: { alg: 'HS512', issuer: "{ alg: 'HS512', key: '[REDACTED]' }", key: '[REDACTED]' }, kind: 'RECORD' }, name: 'access' }], databases: [], users: [] }"#.to_string();
|
||||
let out_str = out.unwrap().to_string();
|
||||
assert_eq!(
|
||||
out_str, out_expected,
|
||||
|
|
|
@ -662,7 +662,7 @@ async fn permissions_checks_remove_ns_access() {
|
|||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, databases: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h\" }, databases: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, databases: { }, users: { } }"],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -704,7 +704,7 @@ async fn permissions_checks_remove_db_access() {
|
|||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -746,7 +746,7 @@ async fn permissions_checks_remove_root_user() {
|
|||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ namespaces: { }, users: { } }"],
|
||||
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER\" } }"],
|
||||
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -788,7 +788,7 @@ async fn permissions_checks_remove_ns_user() {
|
|||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, databases: { }, users: { } }"],
|
||||
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"],
|
||||
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
@ -830,7 +830,7 @@ async fn permissions_checks_remove_db_user() {
|
|||
// Define the expected results for the check statement when the test statement succeeded and when it failed
|
||||
let check_results = [
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"],
|
||||
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"],
|
||||
];
|
||||
|
||||
let test_cases = [
|
||||
|
|
|
@ -40,9 +40,10 @@ async fn info() -> Result<(), Box<dyn std::error::Error>> {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET user = $user, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 24h
|
||||
;
|
||||
"#,
|
||||
)
|
||||
|
@ -84,9 +85,10 @@ async fn signup() -> Result<(), Box<dyn std::error::Error>> {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 24h
|
||||
;"#,
|
||||
)
|
||||
.await?;
|
||||
|
@ -132,9 +134,10 @@ async fn signin() -> Result<(), Box<dyn std::error::Error>> {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 24h
|
||||
;"#,
|
||||
)
|
||||
.await?;
|
||||
|
@ -873,9 +876,10 @@ async fn variable_auth_live_query() -> Result<(), Box<dyn std::error::Error>> {
|
|||
socket_permanent
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1s, FOR TOKEN 24h
|
||||
;"#,
|
||||
)
|
||||
.await?;
|
||||
|
@ -938,9 +942,10 @@ async fn session_expiration() {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1s, FOR TOKEN 1d
|
||||
;"#,
|
||||
)
|
||||
.await
|
||||
|
@ -988,12 +993,12 @@ async fn session_expiration() {
|
|||
assert!(res["result"].is_string(), "result: {:?}", res);
|
||||
let res = res["result"].as_str().unwrap();
|
||||
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
|
||||
// Authenticate using the token, which will expire soon
|
||||
// Authenticate using the token, which expires in a day
|
||||
socket.send_request("authenticate", json!([res,])).await.unwrap();
|
||||
// Check if the session is now authenticated
|
||||
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
|
||||
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
|
||||
// Wait two seconds for token to expire
|
||||
// Wait two seconds for the session to expire
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
// Check that the session has expired and queries fail
|
||||
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
|
||||
|
@ -1048,9 +1053,10 @@ async fn session_expiration_operations() {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1s, FOR TOKEN 1d
|
||||
;"#,
|
||||
)
|
||||
.await
|
||||
|
@ -1098,7 +1104,7 @@ async fn session_expiration_operations() {
|
|||
assert!(res["result"].is_string(), "result: {:?}", res);
|
||||
let res = res["result"].as_str().unwrap();
|
||||
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
|
||||
// Authenticate using the token, which will expire soon
|
||||
// Authenticate using the token, which expires in a day
|
||||
socket.send_request("authenticate", json!([res,])).await.unwrap();
|
||||
// Check if the session is now authenticated
|
||||
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
|
||||
|
@ -1304,9 +1310,10 @@ async fn session_reauthentication() {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1s, FOR TOKEN 24h
|
||||
;"#,
|
||||
)
|
||||
.await
|
||||
|
@ -1392,9 +1399,10 @@ async fn session_reauthentication_expired() {
|
|||
socket
|
||||
.send_message_query(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 1s, FOR TOKEN 24h
|
||||
;"#,
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -706,9 +706,10 @@ mod http_integration {
|
|||
.basic_auth(USER, Some(PASS))
|
||||
.body(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
DURATION FOR SESSION 12h
|
||||
;
|
||||
"#,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue