Improve definition of different durations for authentication (#4119)

This commit is contained in:
Gerard Guillemas Martos 2024-06-05 14:36:41 +02:00 committed by GitHub
parent bf218d9363
commit 20e07a4f79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1295 additions and 296 deletions

View file

@ -10,7 +10,7 @@ use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::AccessType; use crate::sql::AccessType;
use crate::sql::Object; use crate::sql::Object;
use crate::sql::Value; use crate::sql::Value;
use chrono::{Duration, Utc}; use chrono::Utc;
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -148,8 +148,7 @@ pub async fn db_access(
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
// Token expiration is derived from issuer duration exp: expiration(av.duration.token)?,
exp: expiration(iss.duration)?,
jti: Some(Uuid::new_v4().to_string()), jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()), ns: Some(ns.to_owned()),
db: Some(db.to_owned()), db: Some(db.to_owned()),
@ -168,8 +167,7 @@ pub async fn db_access(
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(rid.to_owned())); session.rd = Some(Value::from(rid.to_owned()));
// Session expiration is derived from record access duration session.exp = expiration(av.duration.session)?;
session.exp = expiration(at.duration)?;
session.au = Arc::new(Auth::new(Actor::new( session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(), rid.to_string(),
Default::default(), Default::default(),
@ -215,12 +213,11 @@ pub async fn db_user(
// Create the authentication key // Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref()); let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim // Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims { let val = Claims {
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp, exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()), jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()), ns: Some(ns.to_owned()),
db: Some(db.to_owned()), db: Some(db.to_owned()),
@ -235,7 +232,7 @@ pub async fn db_user(
session.tk = Some(val.into()); session.tk = Some(val.into());
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.db = Some(db.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()); session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
// Check the authentication token // Check the authentication token
match enc { match enc {
@ -260,12 +257,11 @@ pub async fn ns_user(
// Create the authentication key // Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref()); let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim // Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims { let val = Claims {
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp, exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()), jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()), ns: Some(ns.to_owned()),
id: Some(user), id: Some(user),
@ -278,7 +274,7 @@ pub async fn ns_user(
// Set the authentication on the session // Set the authentication on the session
session.tk = Some(val.into()); session.tk = Some(val.into());
session.ns = Some(ns.to_owned()); 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()); session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
// Check the authentication token // Check the authentication token
match enc { match enc {
@ -303,12 +299,11 @@ pub async fn root_user(
// Create the authentication key // Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref()); let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim // Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims { let val = Claims {
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp, exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()), jti: Some(Uuid::new_v4().to_string()),
id: Some(user), id: Some(user),
..Claims::default() ..Claims::default()
@ -319,7 +314,7 @@ pub async fn root_user(
let enc = encode(&HEADER, &val, &key); let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session // Set the authentication on the session
session.tk = Some(val.into()); session.tk = Some(val.into());
session.exp = expiration(u.session)?; session.exp = expiration(u.duration.session)?;
session.au = Arc::new((&u, Level::Root).into()); session.au = Arc::new((&u, Level::Root).into());
// Check the authentication token // Check the authentication token
match enc { match enc {
@ -337,6 +332,8 @@ pub async fn root_user(
mod tests { mod tests {
use super::*; use super::*;
use crate::iam::Role; use crate::iam::Role;
use chrono::Duration;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use std::collections::HashMap; use std::collections::HashMap;
#[tokio::test] #[tokio::test]
@ -347,7 +344,7 @@ mod tests {
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -356,7 +353,9 @@ mod tests {
name: $user, name: $user,
pass: crypto::argon2::generate($pass) pass: crypto::argon2::generate($pass)
} }
); )
DURATION FOR SESSION 2h
;
CREATE user:test CONTENT { CREATE user:test CONTENT {
name: 'user', 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::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::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!(!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(); let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin // 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 min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_exp && exp < max_exp, 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"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -426,7 +425,9 @@ mod tests {
name: $user, name: $user,
pass: crypto::argon2::generate($pass) pass: crypto::argon2::generate($pass)
} }
); )
DURATION FOR SESSION 2h
;
CREATE user:test CONTENT { CREATE user:test CONTENT {
name: 'user', name: 'user',
@ -464,7 +465,6 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_signin_record_with_jwt_issuer() { async fn test_signin_record_with_jwt_issuer() {
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
// Test with correct credentials // Test with correct credentials
{ {
let public_key = r#"-----BEGIN PUBLIC KEY----- let public_key = r#"-----BEGIN PUBLIC KEY-----
@ -510,7 +510,6 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
&format!( &format!(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DEFINE ACCESS user ON DATABASE TYPE RECORD
DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) 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 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 {{ 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::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::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!(!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(); let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin // Expiration should match the current time plus session duration with some margin
let min_sess_exp = 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 = let max_sess_exp =
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_sess_exp && exp < max_sess_exp, 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 // 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(); (Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_tk_exp && exp < max_tk_exp, 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 // Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string())); assert_eq!(token_data.claims.ns, Some("test".to_string()));
@ -619,7 +619,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
#[tokio::test] #[tokio::test]
async fn test_signin_db_user() { async fn test_signin_db_user() {
// //
// Test without roles defined // Test without roles or expiration defined
// //
{ {
let ds = Datastore::new("memory").await.unwrap(); 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 ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); 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 .await
.unwrap(); .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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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 // Test invalid password
@ -720,7 +835,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
#[tokio::test] #[tokio::test]
async fn test_signin_ns_user() { async fn test_signin_ns_user() {
// //
// Test without roles defined // Test without roles or expiration defined
// //
{ {
let ds = Datastore::new("memory").await.unwrap(); 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 ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test"); 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 .await
.unwrap(); .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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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 // Test invalid password
@ -802,7 +1023,7 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
#[tokio::test] #[tokio::test]
async fn test_signin_root_user() { async fn test_signin_root_user() {
// //
// Test without roles defined // Test without roles or expiration defined
// //
{ {
let ds = Datastore::new("memory").await.unwrap(); 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 ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner(); 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 .await
.unwrap(); .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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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 // Test invalid password

View file

@ -87,8 +87,7 @@ pub async fn db_access(
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
// Token expiration is derived from issuer duration exp: expiration(av.duration.token)?,
exp: expiration(iss.duration)?,
jti: Some(Uuid::new_v4().to_string()), jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()), ns: Some(ns.to_owned()),
db: Some(db.to_owned()), db: Some(db.to_owned()),
@ -107,8 +106,7 @@ pub async fn db_access(
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(rid.to_owned())); session.rd = Some(Value::from(rid.to_owned()));
// Session expiration is derived from record access duration session.exp = expiration(av.duration.session)?;
session.exp = expiration(at.duration)?;
session.au = Arc::new(Auth::new(Actor::new( session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(), rid.to_string(),
Default::default(), Default::default(),
@ -156,7 +154,7 @@ mod tests {
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -165,7 +163,9 @@ mod tests {
name: $user, name: $user,
pass: crypto::argon2::generate($pass) pass: crypto::argon2::generate($pass)
} }
); )
DURATION FOR SESSION 2h
;
"#, "#,
&sess, &sess,
None, 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::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::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!(!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(); let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin // 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 min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_exp && exp < max_exp, 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"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -229,7 +229,9 @@ mod tests {
name: $user, name: $user,
pass: crypto::argon2::generate($pass) pass: crypto::argon2::generate($pass)
} }
); )
DURATION FOR SESSION 2h
;
"#, "#,
&sess, &sess,
None, None,
@ -308,7 +310,6 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
&format!( &format!(
r#" r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD DEFINE ACCESS user ON DATABASE TYPE RECORD
DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) 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 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 {{ CREATE user:test CONTENT {{
@ -368,9 +370,9 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
let exp = sess.exp.unwrap(); let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin // Expiration should match the current time plus session duration with some margin
let min_sess_exp = 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 = let max_sess_exp =
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_sess_exp && exp < max_sess_exp, exp > min_sess_exp && exp < max_sess_exp,
"Session expiration is expected to follow access method duration" "Session expiration is expected to follow access method duration"

View file

@ -1,9 +1,8 @@
use crate::dbs::Session; use crate::dbs::Session;
use crate::err::Error; use crate::err::Error;
use crate::iam::issue::expiration;
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
use crate::iam::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::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::access_type::{AccessType, JwtAccessVerify}; use crate::sql::access_type::{AccessType, JwtAccessVerify};
use crate::sql::{statements::DefineUserStatement, Algorithm, Value}; 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 { (Some(ns), Some(db)) => match verify_db_creds(kvs, ns, db, user, pass).await {
Ok(u) => { Ok(u) => {
debug!("Authenticated as database user '{}'", user); 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()); session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
Ok(()) Ok(())
} }
@ -109,7 +108,7 @@ pub async fn basic(
(Some(ns), None) => match verify_ns_creds(kvs, ns, user, pass).await { (Some(ns), None) => match verify_ns_creds(kvs, ns, user, pass).await {
Ok(u) => { Ok(u) => {
debug!("Authenticated as namespace user '{}'", user); 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()); session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
Ok(()) Ok(())
} }
@ -119,7 +118,7 @@ pub async fn basic(
(None, None) => match verify_root_creds(kvs, user, pass).await { (None, None) => match verify_root_creds(kvs, user, pass).await {
Ok(u) => { Ok(u) => {
debug!("Authenticated as root user '{}'", user); 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()); session.au = Arc::new((&u, Level::Root).into());
Ok(()) Ok(())
} }
@ -195,7 +194,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(id.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( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
Default::default(), 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.ns = Some(ns.to_owned());
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.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( session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(), de.name.to_string(),
roles, roles,
@ -287,7 +286,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.tk = Some(value); session.tk = Some(value);
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.db = Some(db.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( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
de.roles.iter().map(|r| r.into()).collect(), 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.tk = Some(value);
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());
session.exp = token_data.claims.exp; session.exp = expiration(de.duration.session)?;
session.au = session.au =
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns)))); Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns))));
Ok(()) Ok(())
@ -372,7 +371,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Set the session // Set the session
session.tk = Some(value); session.tk = Some(value);
session.ns = Some(ns.to_owned()); 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( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
de.roles.iter().map(|r| r.into()).collect(), 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); trace!("Authenticated to root level with user `{}`", id);
// Set the session // Set the session
session.tk = Some(value); session.tk = Some(value);
session.exp = token_data.claims.exp; session.exp = expiration(de.duration.session)?;
session.au = Arc::new(Auth::new(Actor::new( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
de.roles.iter().map(|r| r.into()).collect(), de.roles.iter().map(|r| r.into()).collect(),
@ -524,7 +523,7 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( 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, &sess,
None, 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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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(); let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin // Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::days(1) - Duration::seconds(10)).timestamp(); 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 ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( 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, &sess,
None, None,
) )
@ -694,7 +693,7 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( 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, &sess,
None, None,
) )
@ -761,7 +760,7 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( 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, &sess,
None, 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::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::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!(!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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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 ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( 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(), .as_str(),
&sess, &sess,
None, 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::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::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!(!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::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::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner 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!( format!(
r#" r#"
DEFINE ACCESS token ON DATABASE TYPE RECORD 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; 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::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::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!(!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::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::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!(!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!( format!(
r#" r#"
DEFINE ACCESS token ON DATABASE TYPE RECORD 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; 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::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::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!(!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 { let tk = match sess.tk {
Some(Value::Object(tk)) => tk, Some(Value::Object(tk)) => tk,
_ => panic!("Session token is not an object"), _ => 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::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::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!(!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()); 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);
}
} }

View file

@ -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 revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::ops::Deref; use std::ops::Deref;
use std::str; 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)] #[revisioned(revision = 1)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)] #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]

View file

@ -1,6 +1,6 @@
use crate::sql::statements::info::InfoStructure; use crate::sql::statements::info::InfoStructure;
use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAccessStatement;
use crate::sql::{escape::quote_str, Algorithm, Duration}; use crate::sql::{escape::quote_str, Algorithm};
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; 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)] #[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] #[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
@ -52,8 +75,6 @@ impl Default for JwtAccess {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg, alg,
key, key,
// Defaults to tokens lasting for one hour
duration: Some(Duration::from_hours(1)),
}), }),
} }
} }
@ -90,7 +111,6 @@ impl JwtAccess {
pub struct JwtAccessIssue { pub struct JwtAccessIssue {
pub alg: Algorithm, pub alg: Algorithm,
pub key: String, pub key: String,
pub duration: Option<Duration>,
} }
impl Default for JwtAccessIssue { impl Default for JwtAccessIssue {
@ -100,8 +120,6 @@ impl Default for JwtAccessIssue {
alg: Algorithm::Hs512, alg: Algorithm::Hs512,
// Avoid defaulting to empty key // Avoid defaulting to empty key
key: DefineAccessStatement::random_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)] #[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct RecordAccess { pub struct RecordAccess {
pub duration: Option<Duration>,
pub signup: Option<Value>, pub signup: Option<Value>,
pub signin: Option<Value>, pub signin: Option<Value>,
pub jwt: JwtAccess, pub jwt: JwtAccess,
@ -162,8 +179,6 @@ pub struct RecordAccess {
impl Default for RecordAccess { impl Default for RecordAccess {
fn default() -> Self { fn default() -> Self {
Self { Self {
// Defaults to sessions lasting one hour
duration: Some(Duration::from_hours(1)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -187,9 +202,6 @@ impl Display for AccessType {
} }
AccessType::Record(ac) => { AccessType::Record(ac) => {
f.write_str(" RECORD")?; f.write_str(" RECORD")?;
if let Some(ref v) = ac.duration {
write!(f, " DURATION {v}")?
}
if let Some(ref v) = ac.signup { if let Some(ref v) = ac.signup {
write!(f, " SIGNUP {v}")? write!(f, " SIGNUP {v}")?
} }
@ -219,9 +231,6 @@ impl InfoStructure for AccessType {
if let Some(signin) = ac.signin { if let Some(signin) = ac.signin {
acc.insert("signin".to_string(), signin.structure()); 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()); acc.insert("jwt".to_string(), ac.jwt.structure());
} }
}; };
@ -242,9 +251,6 @@ impl Display for JwtAccess {
} }
if let Some(iss) = &self.issue { if let Some(iss) = &self.issue {
write!(f, " WITH ISSUER KEY {}", quote_str(&iss.key))?; write!(f, " WITH ISSUER KEY {}", quote_str(&iss.key))?;
if let Some(ref v) = iss.duration {
write!(f, " DURATION {v}")?
}
} }
Ok(()) Ok(())
} }
@ -266,9 +272,6 @@ impl InfoStructure for JwtAccess {
let mut iss = Object::default(); let mut iss = Object::default();
iss.insert("alg".to_string(), v.alg.structure()); iss.insert("alg".to_string(), v.alg.structure());
iss.insert("key".to_string(), v.key.into()); 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()); acc.insert("issuer".to_string(), iss.to_string().into());
} }
Value::Object(acc) Value::Object(acc)

View file

@ -64,6 +64,7 @@ pub(crate) mod table_type;
pub(crate) mod thing; pub(crate) mod thing;
pub(crate) mod timeout; pub(crate) mod timeout;
pub(crate) mod tokenizer; pub(crate) mod tokenizer;
pub(crate) mod user;
pub(crate) mod uuid; pub(crate) mod uuid;
pub(crate) mod value; pub(crate) mod value;
pub(crate) mod version; pub(crate) mod version;

View file

@ -4,7 +4,7 @@ use crate::doc::CursorDoc;
use crate::err::Error; use crate::err::Error;
use crate::iam::{Action, ResourceKind}; use crate::iam::{Action, ResourceKind};
use crate::sql::statements::info::InfoStructure; 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 derive::Store;
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use rand::Rng; use rand::Rng;
@ -12,7 +12,7 @@ use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
#[revisioned(revision = 2)] #[revisioned(revision = 1)]
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
@ -20,8 +20,8 @@ pub struct DefineAccessStatement {
pub name: Ident, pub name: Ident,
pub base: Base, pub base: Base,
pub kind: AccessType, pub kind: AccessType,
pub duration: AccessDuration,
pub comment: Option<Strand>, pub comment: Option<Strand>,
#[revision(start = 2)]
pub if_not_exists: bool, pub if_not_exists: bool,
} }
@ -132,9 +132,6 @@ impl Display for DefineAccessStatement {
} }
AccessType::Record(ac) => { AccessType::Record(ac) => {
write!(f, " TYPE RECORD")?; write!(f, " TYPE RECORD")?;
if let Some(ref v) = ac.duration {
write!(f, " DURATION {v}")?
}
if let Some(ref v) = ac.signup { if let Some(ref v) = ac.signup {
write!(f, " SIGNUP {v}")? write!(f, " SIGNUP {v}")?
} }
@ -144,6 +141,38 @@ impl Display for DefineAccessStatement {
write!(f, " WITH JWT {}", ac.jwt)?; 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 { if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")? write!(f, " COMMENT {v}")?
} }
@ -157,6 +186,7 @@ impl InfoStructure for DefineAccessStatement {
name, name,
base, base,
kind, kind,
duration,
comment, comment,
.. ..
} = self; } = self;
@ -166,6 +196,16 @@ impl InfoStructure for DefineAccessStatement {
acc.insert("base".to_string(), base.structure()); 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()); acc.insert("kind".to_string(), kind.structure());
if let Some(comment) = comment { if let Some(comment) = comment {

View file

@ -4,7 +4,9 @@ use crate::doc::CursorDoc;
use crate::err::Error; use crate::err::Error;
use crate::iam::{Action, ResourceKind}; use crate::iam::{Action, ResourceKind};
use crate::sql::statements::info::InfoStructure; 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::{ use argon2::{
password_hash::{PasswordHasher, SaltString}, password_hash::{PasswordHasher, SaltString},
Argon2, Argon2,
@ -26,7 +28,7 @@ pub struct DefineUserStatement {
pub code: String, pub code: String,
pub roles: Vec<Ident>, pub roles: Vec<Ident>,
#[revision(start = 3)] #[revision(start = 3)]
pub session: Option<Duration>, pub duration: UserDuration,
pub comment: Option<Strand>, pub comment: Option<Strand>,
#[revision(start = 2)] #[revision(start = 2)]
pub if_not_exists: bool, pub if_not_exists: bool,
@ -47,7 +49,7 @@ impl From<(Base, &str, &str, &str)> for DefineUserStatement {
.map(char::from) .map(char::from)
.collect::<String>(), .collect::<String>(),
roles: vec![role.into()], roles: vec![role.into()],
session: None, duration: UserDuration::default(),
comment: None, comment: None,
if_not_exists: false, if_not_exists: false,
} }
@ -59,13 +61,13 @@ impl DefineUserStatement {
name: Ident, name: Ident,
base: Base, base: Base,
roles: Vec<Ident>, roles: Vec<Ident>,
session: Option<Duration>, duration: UserDuration,
) -> Self { ) -> Self {
DefineUserStatement { DefineUserStatement {
name, name,
base, base,
roles, // New users get the viewer role by default roles,
session, // Sessions for system users do not expire by default duration,
code: rand::thread_rng() code: rand::thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)
.take(128) .take(128)
@ -86,8 +88,12 @@ impl DefineUserStatement {
self.hash = passhash; self.hash = passhash;
} }
pub(crate) fn set_session(&mut self, session: Option<Duration>) { pub(crate) fn set_token_duration(&mut self, duration: Option<Duration>) {
self.session = session; 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 /// 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>>() &self.roles.iter().map(|r| r.to_string().to_uppercase()).collect::<Vec<String>>()
), ),
)?; )?;
if let Some(ref v) = self.session { // Always print relevant durations so defaults can be changed in the future
write!(f, " SESSION {v}")? // 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 { if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")? write!(f, " COMMENT {v}")?
} }
@ -223,7 +246,7 @@ impl InfoStructure for DefineUserStatement {
base, base,
hash, hash,
roles, roles,
session, duration,
comment, comment,
.. ..
} = self; } = self;
@ -240,9 +263,10 @@ impl InfoStructure for DefineUserStatement {
Value::Array(roles.into_iter().map(|r| r.structure()).collect()), Value::Array(roles.into_iter().map(|r| r.structure()).collect()),
); );
if let Some(session) = session { let mut dur = Object::default();
acc.insert("session".to_string(), session.into()); 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 { if let Some(comment) = comment {
acc.insert("comment".to_string(), comment.into()); acc.insert("comment".to_string(), comment.into());

27
core/src/sql/user.rs Normal file
View 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,
}
}
}

View file

@ -5,7 +5,6 @@ use crate::sql::access_type::{
}; };
use crate::sql::value::serde::ser; use crate::sql::value::serde::ser;
use crate::sql::Algorithm; use crate::sql::Algorithm;
use crate::sql::Duration;
use crate::sql::Value; use crate::sql::Value;
use ser::Serializer as _; use ser::Serializer as _;
use serde::ser::Error as _; use serde::ser::Error as _;
@ -82,7 +81,6 @@ impl ser::Serializer for SerializerRecord {
#[derive(Default)] #[derive(Default)]
#[non_exhaustive] #[non_exhaustive]
pub struct SerializeRecord { pub struct SerializeRecord {
pub duration: Option<Duration>,
pub signup: Option<Value>, pub signup: Option<Value>,
pub signin: Option<Value>, pub signin: Option<Value>,
pub jwt: JwtAccess, pub jwt: JwtAccess,
@ -97,10 +95,6 @@ impl serde::ser::SerializeStruct for SerializeRecord {
T: ?Sized + Serialize, T: ?Sized + Serialize,
{ {
match key { match key {
"duration" => {
self.duration =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
"signup" => { "signup" => {
self.signup = value.serialize(ser::value::opt::Serializer.wrap())?; 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> { fn end(self) -> Result<Self::Ok, Error> {
Ok(RecordAccess { Ok(RecordAccess {
duration: self.duration,
signup: self.signup, signup: self.signup,
signin: self.signin, signin: self.signin,
jwt: self.jwt, jwt: self.jwt,
@ -420,7 +413,6 @@ impl ser::Serializer for SerializerJwtIssue {
pub struct SerializeJwtIssue { pub struct SerializeJwtIssue {
pub alg: Algorithm, pub alg: Algorithm,
pub key: String, pub key: String,
pub duration: Option<Duration>,
} }
impl serde::ser::SerializeStruct for SerializeJwtIssue { impl serde::ser::SerializeStruct for SerializeJwtIssue {
@ -438,10 +430,6 @@ impl serde::ser::SerializeStruct for SerializeJwtIssue {
"key" => { "key" => {
self.key = value.serialize(ser::string::Serializer.wrap())?; self.key = value.serialize(ser::string::Serializer.wrap())?;
} }
"duration" => {
self.duration =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
key => { key => {
return Err(Error::custom(format!("unexpected field `JwtAccessIssue::{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> { fn end(self) -> Result<Self::Ok, Error> {
Ok(JwtAccessIssue { Ok(JwtAccessIssue {
duration: self.duration,
alg: self.alg, alg: self.alg,
key: self.key, key: self.key,
}) })

View file

@ -1,8 +1,10 @@
use crate::err::Error; use crate::err::Error;
use crate::sql::access::AccessDuration;
use crate::sql::access_type::AccessType; use crate::sql::access_type::AccessType;
use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAccessStatement;
use crate::sql::value::serde::ser; use crate::sql::value::serde::ser;
use crate::sql::Base; use crate::sql::Base;
use crate::sql::Duration;
use crate::sql::Ident; use crate::sql::Ident;
use crate::sql::Strand; use crate::sql::Strand;
use ser::Serializer as _; use ser::Serializer as _;
@ -43,6 +45,7 @@ pub struct SerializeDefineAccessStatement {
name: Ident, name: Ident,
base: Base, base: Base,
kind: AccessType, kind: AccessType,
duration: AccessDuration,
comment: Option<Strand>, comment: Option<Strand>,
if_not_exists: bool, if_not_exists: bool,
} }
@ -65,6 +68,9 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
"kind" => { "kind" => {
self.kind = value.serialize(ser::access_type::Serializer.wrap())?; self.kind = value.serialize(ser::access_type::Serializer.wrap())?;
} }
"duration" => {
self.duration = value.serialize(SerializerDuration.wrap())?;
}
"comment" => { "comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?; self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
} }
@ -85,12 +91,84 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
name: self.name, name: self.name,
base: self.base, base: self.base,
kind: self.kind, kind: self.kind,
duration: self.duration,
comment: self.comment, comment: self.comment,
if_not_exists: self.if_not_exists, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,5 +1,6 @@
use crate::err::Error; use crate::err::Error;
use crate::sql::statements::DefineUserStatement; use crate::sql::statements::DefineUserStatement;
use crate::sql::user::UserDuration;
use crate::sql::value::serde::ser; use crate::sql::value::serde::ser;
use crate::sql::Base; use crate::sql::Base;
use crate::sql::Duration; use crate::sql::Duration;
@ -45,7 +46,7 @@ pub struct SerializeDefineUserStatement {
hash: String, hash: String,
code: String, code: String,
roles: Vec<Ident>, roles: Vec<Ident>,
session: Option<Duration>, duration: UserDuration,
comment: Option<Strand>, comment: Option<Strand>,
if_not_exists: bool, if_not_exists: bool,
} }
@ -74,9 +75,8 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
"roles" => { "roles" => {
self.roles = value.serialize(ser::ident::vec::Serializer.wrap())?; self.roles = value.serialize(ser::ident::vec::Serializer.wrap())?;
} }
"session" => { "duration" => {
self.session = self.duration = value.serialize(SerializerDuration.wrap())?;
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
} }
"comment" => { "comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?; self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
@ -100,12 +100,76 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
hash: self.hash, hash: self.hash,
code: self.code, code: self.code,
roles: self.roles, roles: self.roles,
session: self.session, duration: self.duration,
comment: self.comment, comment: self.comment,
if_not_exists: self.if_not_exists, 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)] #[cfg(test)]
mod tests { mod tests {
@ -117,4 +181,17 @@ mod tests {
let value: DefineUserStatement = stmt.serialize(Serializer.wrap()).unwrap(); let value: DefineUserStatement = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(value, stmt); 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);
}
} }

View file

@ -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 { impl From<Id> for Value {
fn from(v: Id) -> Self { fn from(v: Id) -> Self {
match v { match v {

View file

@ -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("FROM") => TokenKind::Keyword(Keyword::From),
UniCase::ascii("FULL") => TokenKind::Keyword(Keyword::Full), UniCase::ascii("FULL") => TokenKind::Keyword(Keyword::Full),
UniCase::ascii("FUNCTION") => TokenKind::Keyword(Keyword::Function), UniCase::ascii("FUNCTION") => TokenKind::Keyword(Keyword::Function),
UniCase::ascii("GRANT") => TokenKind::Keyword(Keyword::Grant),
UniCase::ascii("GROUP") => TokenKind::Keyword(Keyword::Group), UniCase::ascii("GROUP") => TokenKind::Keyword(Keyword::Group),
UniCase::ascii("HIGHLIGHTS") => TokenKind::Keyword(Keyword::Highlights), UniCase::ascii("HIGHLIGHTS") => TokenKind::Keyword(Keyword::Highlights),
UniCase::ascii("HNSW") => TokenKind::Keyword(Keyword::Hnsw), UniCase::ascii("HNSW") => TokenKind::Keyword(Keyword::Hnsw),

View file

@ -16,8 +16,8 @@ use crate::{
}, },
table_type, table_type,
tokenizer::Tokenizer, tokenizer::Tokenizer,
AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType, user, AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand,
Values, TableType, Values,
}, },
syn::{ syn::{
parser::{ parser::{
@ -182,7 +182,7 @@ impl Parser<'_> {
name, name,
base, base,
vec!["Viewer".into()], // New users get the viewer role by default 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 { if if_not_exists {
@ -210,9 +210,35 @@ impl Parser<'_> {
res.roles.push(self.next_token_value()?); res.roles.push(self.next_token_value()?);
} }
} }
t!("SESSION") => { t!("DURATION") => {
self.pop_peek(); 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, _ => break,
} }
@ -255,7 +281,7 @@ impl Parser<'_> {
match self.peek_kind() { match self.peek_kind() {
t!("JWT") => { t!("JWT") => {
self.pop_peek(); self.pop_peek();
res.kind = AccessType::Jwt(self.parse_jwt(None)?); res.kind = AccessType::Jwt(self.parse_jwt()?);
} }
t!("RECORD") => { t!("RECORD") => {
self.pop_peek(); self.pop_peek();
@ -264,15 +290,6 @@ impl Parser<'_> {
}; };
loop { loop {
match self.peek_kind() { 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") => { t!("SIGNUP") => {
self.pop_peek(); self.pop_peek();
ac.signup = ac.signup =
@ -288,13 +305,55 @@ impl Parser<'_> {
} }
if self.eat(t!("WITH")) { if self.eat(t!("WITH")) {
expected!(self, t!("JWT")); expected!(self, t!("JWT"));
ac.jwt = self.parse_jwt(Some(AccessType::Record(ac.clone())))?; ac.jwt = self.parse_jwt()?;
} }
res.kind = AccessType::Record(ac); res.kind = AccessType::Record(ac);
} }
_ => break, _ => 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, _ => break,
} }
} }
@ -444,11 +503,7 @@ impl Parser<'_> {
} }
t!("SESSION") => { t!("SESSION") => {
self.pop_peek(); self.pop_peek();
ac.duration = Some(self.next_token_value()?); res.duration.session = 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;
}
} }
t!("SIGNUP") => { t!("SIGNUP") => {
self.pop_peek(); self.pop_peek();
@ -1083,7 +1138,7 @@ impl Parser<'_> {
Ok(Kind::Record(names)) 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 { let mut res = access_type::JwtAccess {
// By default, a JWT access method is only used to verify. // By default, a JWT access method is only used to verify.
issue: None, issue: None,
@ -1094,13 +1149,6 @@ impl Parser<'_> {
..Default::default() ..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() { match self.peek_kind() {
t!("ALGORITHM") => { t!("ALGORITHM") => {
self.pop_peek(); self.pop_peek();
@ -1176,10 +1224,6 @@ impl Parser<'_> {
} }
iss.key = key; iss.key = key;
} }
t!("DURATION") => {
self.pop_peek();
iss.duration = Some(self.next_token_value()?);
}
_ => break, _ => break,
} }
} }

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
sql::{ sql::{
access::AccessDuration,
access_type::{ access_type::{
AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks, AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks,
JwtAccessVerifyKey, RecordAccess, JwtAccessVerifyKey, RecordAccess,
@ -10,7 +11,9 @@ use crate::{
index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType}, index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType},
language::Language, language::Language,
statements::{ statements::{
analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement, analyze::AnalyzeStatement,
show::{ShowSince, ShowStatement},
sleep::SleepStatement,
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement,
CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
@ -25,6 +28,7 @@ use crate::{
UseStatement, UseStatement,
}, },
tokenizer::Tokenizer, tokenizer::Tokenizer,
user::UserDuration,
Algorithm, Array, Base, Block, Cond, Data, Datetime, Dir, Duration, Edges, Explain, Algorithm, Array, Base, Block, Cond, Data, Datetime, Dir, Duration, Edges, Explain,
Expression, Fetch, Fetchs, Field, Fields, Future, Graph, Group, Groups, Id, Ident, Idiom, Expression, Fetch, Fetchs, Field, Fields, Future, Graph, Group, Groups, Id, Ident, Idiom,
Idioms, Index, Kind, Limit, Number, Object, Operator, Order, Orders, Output, Param, Part, Idioms, Index, Kind, Limit, Number, Object, Operator, Order, Orders, Output, Param, Part,
@ -206,21 +210,140 @@ fn parse_define_function() {
#[test] #[test]
fn parse_define_user() { fn parse_define_user() {
let res = test_parse!( // Password.
parse_stmt, {
r#"DEFINE USER user ON ROOT COMMENT 'test' PASSHASH 'hunter2' ROLES foo, bar COMMENT "*******""# let res = test_parse!(
) parse_stmt,
.unwrap(); r#"DEFINE USER user ON ROOT COMMENT 'test' PASSWORD 'hunter2' COMMENT "*******""#
)
.unwrap();
let Statement::Define(DefineStatement::User(stmt)) = res else { let Statement::Define(DefineStatement::User(stmt)) = res else {
panic!() panic!()
}; };
assert_eq!(stmt.name, Ident("user".to_string())); assert_eq!(stmt.name, Ident("user".to_string()));
assert_eq!(stmt.base, Base::Root); assert_eq!(stmt.base, Base::Root);
assert_eq!(stmt.hash, "hunter2".to_owned()); assert!(stmt.hash.starts_with("$argon2id$"));
assert_eq!(stmt.roles, vec![Ident("foo".to_string()), Ident("bar".to_string())]); assert_eq!(stmt.roles, vec![Ident("Viewer".to_string())]);
assert_eq!(stmt.comment, Some(Strand("*******".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. // 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, issue: None,
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, 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.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); // Scope base is ignored. 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.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false); assert_eq!(stmt.if_not_exists, false);
match stmt.kind { match stmt.kind {
AccessType::Record(ac) => { 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.signup, None);
assert_eq!(ac.signin, None); assert_eq!(ac.signin, None);
match ac.jwt.verify { match ac.jwt.verify {
@ -305,6 +441,12 @@ fn parse_define_token_jwks() {
}), }),
issue: None, issue: None,
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, 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.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); // Scope base is ignored. 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.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false); assert_eq!(stmt.if_not_exists, false);
match stmt.kind { match stmt.kind {
AccessType::Record(ac) => { 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.signup, None);
assert_eq!(ac.signin, None); assert_eq!(ac.signin, None);
match ac.jwt.verify { match ac.jwt.verify {
@ -366,10 +515,17 @@ fn parse_define_scope() {
assert_eq!(stmt.name, Ident("a".to_string())); assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.comment, Some(Strand("bar".to_string()))); 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); assert_eq!(stmt.if_not_exists, false);
match stmt.kind { match stmt.kind {
AccessType::Record(ac) => { 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.signup, Some(Value::Bool(true)));
assert_eq!(ac.signin, Some(Value::Bool(false))); assert_eq!(ac.signin, Some(Value::Bool(false)));
match ac.jwt.verify { match ac.jwt.verify {
@ -381,8 +537,6 @@ fn parse_define_scope() {
match ac.jwt.issue { match ac.jwt.issue {
Some(iss) => { Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512); assert_eq!(iss.alg, Algorithm::Hs512);
// Token duration matches session duration by default.
assert_eq!(iss.duration, Some(Duration::from_secs(1)));
} }
_ => panic!(), _ => panic!(),
} }
@ -412,6 +566,12 @@ fn parse_define_access_jwt_key() {
}), }),
issue: None, issue: None,
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})), })),
@ -437,10 +597,14 @@ fn parse_define_access_jwt_key() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::EdDSA, alg: Algorithm::EdDSA,
key: "bar".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -466,20 +630,24 @@ fn parse_define_access_jwt_key() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256, alg: Algorithm::Hs256,
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
) )
} }
// Symmetric verify and explicit issue. // Symmetric verify and explicit duration.
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -495,9 +663,13 @@ fn parse_define_access_jwt_key() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256, alg: Algorithm::Hs256,
key: "foo".to_string(), key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}), }),
}), }),
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
session: None,
},
comment: None, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -507,7 +679,7 @@ fn parse_define_access_jwt_key() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -523,9 +695,14 @@ fn parse_define_access_jwt_key() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256, alg: Algorithm::Hs256,
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -535,7 +712,7 @@ fn parse_define_access_jwt_key() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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!( assert!(
res.is_err(), res.is_err(),
@ -547,7 +724,7 @@ fn parse_define_access_jwt_key() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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!( assert!(
res.is_err(), res.is_err(),
@ -559,7 +736,7 @@ fn parse_define_access_jwt_key() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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!( assert!(
res.is_err(), res.is_err(),
@ -567,6 +744,18 @@ fn parse_define_access_jwt_key() {
res 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] #[test]
@ -589,6 +778,12 @@ fn parse_define_access_jwt_jwks() {
}), }),
issue: None, issue: None,
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})), })),
@ -613,10 +808,14 @@ fn parse_define_access_jwt_jwks() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Hs384, alg: Algorithm::Hs384,
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -626,7 +825,7 @@ fn parse_define_access_jwt_jwks() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -641,9 +840,13 @@ fn parse_define_access_jwt_jwks() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Hs384, alg: Algorithm::Hs384,
key: "foo".to_string(), key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}), }),
}), }),
duration: AccessDuration {
grant: None,
token: Some(Duration::from_secs(10)),
session: None,
},
comment: None, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -668,10 +871,14 @@ fn parse_define_access_jwt_jwks() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Ps256, alg: Algorithm::Ps256,
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -681,7 +888,7 @@ fn parse_define_access_jwt_jwks() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -696,9 +903,13 @@ fn parse_define_access_jwt_jwks() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Ps256, alg: Algorithm::Ps256,
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -721,12 +932,19 @@ fn parse_define_access_record() {
assert_eq!(stmt.name, Ident("a".to_string())); assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); 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.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false); assert_eq!(stmt.if_not_exists, false);
match stmt.kind { match stmt.kind {
AccessType::Record(ac) => { 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.signup, None);
assert_eq!(ac.signin, None); assert_eq!(ac.signin, None);
match ac.jwt.verify { match ac.jwt.verify {
@ -738,8 +956,6 @@ fn parse_define_access_record() {
match ac.jwt.issue { match ac.jwt.issue {
Some(iss) => { Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512); 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!(), _ => panic!(),
} }
@ -747,11 +963,11 @@ fn parse_define_access_record() {
_ => panic!(), _ => panic!(),
} }
} }
// Duration and signing queries are explicitly defined. // Session duration and signing queries are explicitly defined.
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
@ -763,11 +979,18 @@ fn parse_define_access_record() {
assert_eq!(stmt.name, Ident("a".to_string())); assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); 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.comment, None);
assert_eq!(stmt.if_not_exists, false); assert_eq!(stmt.if_not_exists, false);
match stmt.kind { match stmt.kind {
AccessType::Record(ac) => { 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.signup, Some(Value::Bool(true)));
assert_eq!(ac.signin, Some(Value::Bool(false))); assert_eq!(ac.signin, Some(Value::Bool(false)));
match ac.jwt.verify { match ac.jwt.verify {
@ -779,8 +1002,6 @@ fn parse_define_access_record() {
match ac.jwt.issue { match ac.jwt.issue {
Some(iss) => { Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512); assert_eq!(iss.alg, Algorithm::Hs512);
// Token duration matches session duration by default.
assert_eq!(iss.duration, Some(Duration::from_secs(10)));
} }
_ => panic!(), _ => panic!(),
} }
@ -792,7 +1013,7 @@ fn parse_define_access_record() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -801,7 +1022,6 @@ fn parse_define_access_record() {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Db, base: Base::Db,
kind: AccessType::Record(RecordAccess { kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -813,11 +1033,14 @@ fn parse_define_access_record() {
alg: Algorithm::Hs384, alg: Algorithm::Hs384,
// Issuer key matches verification key by default in symmetric algorithms. // Issuer key matches verification key by default in symmetric algorithms.
key: "foo".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -827,7 +1050,7 @@ fn parse_define_access_record() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -836,7 +1059,6 @@ fn parse_define_access_record() {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Db, base: Base::Db,
kind: AccessType::Record(RecordAccess { kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -847,11 +1069,14 @@ fn parse_define_access_record() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Ps512, alg: Algorithm::Ps512,
key: "bar".to_string(), 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, comment: None,
if_not_exists: false, if_not_exists: false,
})), })),
@ -861,7 +1086,7 @@ fn parse_define_access_record() {
{ {
let res = test_parse!( let res = test_parse!(
parse_stmt, 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(); .unwrap();
assert_eq!( assert_eq!(
@ -870,7 +1095,6 @@ fn parse_define_access_record() {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Db, base: Base::Db,
kind: AccessType::Record(RecordAccess { kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -881,15 +1105,29 @@ fn parse_define_access_record() {
issue: Some(JwtAccessIssue { issue: Some(JwtAccessIssue {
alg: Algorithm::Rs256, alg: Algorithm::Rs256,
key: "bar".to_string(), 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, comment: None,
if_not_exists: false, 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() { fn parse_define_access_record_with_jwt() {
@ -904,7 +1142,6 @@ fn parse_define_access_record_with_jwt() {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Db, base: Base::Db,
kind: AccessType::Record(RecordAccess { kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_hours(1)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -915,6 +1152,12 @@ fn parse_define_access_record_with_jwt() {
issue: None, issue: None,
} }
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})), })),

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
sql::{ sql::{
access::AccessDuration,
access_type::{AccessType, JwtAccess, JwtAccessVerify, JwtAccessVerifyKey, RecordAccess}, access_type::{AccessType, JwtAccess, JwtAccessVerify, JwtAccessVerifyKey, RecordAccess},
block::Entry, block::Entry,
changefeed::ChangeFeed, changefeed::ChangeFeed,
@ -195,7 +196,6 @@ fn statements() -> Vec<Statement> {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Db, base: Base::Db,
kind: AccessType::Record(RecordAccess { kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_hours(1)),
signup: None, signup: None,
signin: None, signin: None,
jwt: JwtAccess { jwt: JwtAccess {
@ -206,6 +206,12 @@ fn statements() -> Vec<Statement> {
issue: None, issue: None,
}, },
}), }),
// Default durations.
duration: AccessDuration {
grant: None,
token: Some(Duration::from_hours(1)),
session: None,
},
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})), })),

View file

@ -84,6 +84,7 @@ keyword! {
From => "FROM", From => "FROM",
Full => "FULL", Full => "FULL",
Function => "FUNCTION", Function => "FUNCTION",
Grant => "GRANT",
Group => "GROUP", Group => "GROUP",
Highlights => "HIGHLIGHTS", Highlights => "HIGHLIGHTS",
Hnsw => "HNSW", Hnsw => "HNSW",

View file

@ -59,9 +59,10 @@ async fn signup_record() {
let access = Ulid::new().to_string(); let access = Ulid::new().to_string();
let sql = format!( 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $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(); let response = db.query(sql).await.unwrap();
@ -130,9 +131,10 @@ async fn signin_record() {
let pass = "password123"; let pass = "password123";
let sql = format!( 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $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(); let response = db.query(sql).await.unwrap();
@ -172,9 +174,10 @@ async fn record_access_throws_error() {
let pass = "password123"; let pass = "password123";
let sql = format!( let sql = format!(
" "
DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s DEFINE ACCESS `{access}` ON DB TYPE RECORD
SIGNUP {{ THROW 'signup_thrown_error' }} SIGNUP {{ THROW 'signup_thrown_error' }}
SIGNIN {{ THROW 'signin_thrown_error' }} SIGNIN {{ THROW 'signin_thrown_error' }}
DURATION FOR SESSION 1d FOR TOKEN 15s
" "
); );
let response = db.query(sql).await.unwrap(); let response = db.query(sql).await.unwrap();
@ -234,9 +237,10 @@ async fn record_access_invalid_query() {
let pass = "password123"; let pass = "password123";
let sql = format!( 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] }} SIGNUP {{ SELECT * FROM ONLY [1, 2] }}
SIGNIN {{ 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(); let response = db.query(sql).await.unwrap();

View file

@ -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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] 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() { async fn permissions_checks_define_user_root() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("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"), ("check", "INFO FOR ROOT"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] vec!["{ namespaces: { }, users: { } }"]
]; ];
@ -1824,13 +1824,13 @@ async fn permissions_checks_define_user_root() {
async fn permissions_checks_define_user_ns() { async fn permissions_checks_define_user_ns() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("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"), ("check", "INFO FOR NS"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] vec!["{ accesses: { }, databases: { }, users: { } }"]
]; ];
@ -1866,13 +1866,13 @@ async fn permissions_checks_define_user_ns() {
async fn permissions_checks_define_user_db() { async fn permissions_checks_define_user_db() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("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"), ("check", "INFO FOR DB"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] 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() { async fn permissions_checks_define_access_record() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("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"), ("check", "INFO FOR DB"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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: { } }"] 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] #[tokio::test]
async fn redefining_existing_user_should_not_error() -> Result<(), Error> { async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
let sql = " let sql = "
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 SESSION 1d; DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h;
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); 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] #[tokio::test]
async fn redefining_existing_user_with_if_not_exists_should_error() -> Result<(), Error> { async fn redefining_existing_user_with_if_not_exists_should_error() -> Result<(), Error> {
let sql = " 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 DURATION FOR TOKEN 15m, FOR SESSION 6h;
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;
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); let ses = Session::owner().with_ns("test").with_db("test");

View file

@ -69,7 +69,7 @@ async fn info_for_db() {
let sql = r#" let sql = r#"
DEFINE TABLE TB; DEFINE TABLE TB;
DEFINE ACCESS jwt ON DB TYPE JWT ALGORITHM HS512 KEY 'secret'; 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 USER user ON DB PASSWORD 'pass';
DEFINE FUNCTION fn::greet() {RETURN "Hello";}; DEFINE FUNCTION fn::greet() {RETURN "Hello";};
DEFINE PARAM $param VALUE "foo"; DEFINE PARAM $param VALUE "foo";
@ -363,15 +363,15 @@ async fn permissions_checks_info_table() {
#[tokio::test] #[tokio::test]
async fn permissions_checks_info_user_root() { async fn permissions_checks_info_user_root() {
let scenario = HashMap::from([ 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"), ("test", "INFO FOR USER user ON ROOT"),
("check", "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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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 DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
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\""],
]; ];
let test_cases = [ let test_cases = [
@ -405,15 +405,15 @@ async fn permissions_checks_info_user_root() {
#[tokio::test] #[tokio::test]
async fn permissions_checks_info_user_ns() { async fn permissions_checks_info_user_ns() {
let scenario = HashMap::from([ 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"), ("test", "INFO FOR USER user ON NS"),
("check", "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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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 DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
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\""],
]; ];
let test_cases = [ let test_cases = [
@ -447,15 +447,15 @@ async fn permissions_checks_info_user_ns() {
#[tokio::test] #[tokio::test]
async fn permissions_checks_info_user_db() { async fn permissions_checks_info_user_db() {
let scenario = HashMap::from([ 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"), ("test", "INFO FOR USER user ON DB"),
("check", "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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ 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 DURATION FOR TOKEN 15m, FOR SESSION 6h\""],
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\""],
]; ];
let test_cases = [ let test_cases = [
@ -504,7 +504,7 @@ async fn access_info_redacted() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -527,7 +527,7 @@ async fn access_info_redacted() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -550,7 +550,7 @@ async fn access_info_redacted() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -564,7 +564,7 @@ async fn access_info_redacted_structure() {
// Symmetric // Symmetric
{ {
let sql = r#" 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 INFO FOR NS STRUCTURE
"#; "#;
let dbs = new_ds().await.unwrap(); let dbs = new_ds().await.unwrap();
@ -577,7 +577,7 @@ async fn access_info_redacted_structure() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -587,7 +587,7 @@ async fn access_info_redacted_structure() {
// Asymmetric // Asymmetric
{ {
let sql = r#" 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 INFO FOR NS STRUCTURE
"#; "#;
let dbs = new_ds().await.unwrap(); let dbs = new_ds().await.unwrap();
@ -600,7 +600,7 @@ async fn access_info_redacted_structure() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -610,7 +610,7 @@ async fn access_info_redacted_structure() {
// Record // Record
{ {
let sql = r#" 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 INFO FOR NS STRUCTURE
"#; "#;
let dbs = new_ds().await.unwrap(); let dbs = new_ds().await.unwrap();
@ -623,7 +623,7 @@ async fn access_info_redacted_structure() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,

View file

@ -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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ accesses: { }, databases: { }, users: { } }"], 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 = [ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], 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 = [ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ namespaces: { }, users: { } }"], 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 = [ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ accesses: { }, databases: { }, users: { } }"], 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 = [ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], 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 = [ let test_cases = [

View file

@ -40,9 +40,10 @@ async fn info() -> Result<(), Box<dyn std::error::Error>> {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET user = $user, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $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 socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 24h
;"#, ;"#,
) )
.await?; .await?;
@ -132,9 +134,10 @@ async fn signin() -> Result<(), Box<dyn std::error::Error>> {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 24h
;"#, ;"#,
) )
.await?; .await?;
@ -873,9 +876,10 @@ async fn variable_auth_live_query() -> Result<(), Box<dyn std::error::Error>> {
socket_permanent socket_permanent
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 1s, FOR TOKEN 24h
;"#, ;"#,
) )
.await?; .await?;
@ -938,9 +942,10 @@ async fn session_expiration() {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 1s, FOR TOKEN 1d
;"#, ;"#,
) )
.await .await
@ -988,12 +993,12 @@ async fn session_expiration() {
assert!(res["result"].is_string(), "result: {:?}", res); assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap(); let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res); 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(); socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check if the session is now authenticated // Check if the session is now authenticated
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap(); let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res); 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; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Check that the session has expired and queries fail // Check that the session has expired and queries fail
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await; let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
@ -1048,9 +1053,10 @@ async fn session_expiration_operations() {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 1s, FOR TOKEN 1d
;"#, ;"#,
) )
.await .await
@ -1098,7 +1104,7 @@ async fn session_expiration_operations() {
assert!(res["result"].is_string(), "result: {:?}", res); assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap(); let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res); 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(); socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check if the session is now authenticated // Check if the session is now authenticated
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap(); let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
@ -1304,9 +1310,10 @@ async fn session_reauthentication() {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 1s, FOR TOKEN 24h
;"#, ;"#,
) )
.await .await
@ -1392,9 +1399,10 @@ async fn session_reauthentication_expired() {
socket socket
.send_message_query( .send_message_query(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 1s, FOR TOKEN 24h
;"#, ;"#,
) )
.await .await

View file

@ -706,9 +706,10 @@ mod http_integration {
.basic_auth(USER, Some(PASS)) .basic_auth(USER, Some(PASS))
.body( .body(
r#" 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) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
DURATION FOR SESSION 12h
; ;
"#, "#,
) )