Implement session expiration ()

This commit is contained in:
Gerard Guillemas Martos 2024-03-05 16:26:14 +01:00 committed by GitHub
parent e5c63234ca
commit 957eff19a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 647 additions and 12 deletions
core/src
tests/common

View file

@ -2,6 +2,7 @@ use crate::ctx::Context;
use crate::iam::Auth;
use crate::iam::{Level, Role};
use crate::sql::value::Value;
use chrono::Utc;
use std::sync::Arc;
/// Specifies the current session information when processing a query.
@ -27,6 +28,8 @@ pub struct Session {
pub tk: Option<Value>,
/// The current scope authentication data
pub sd: Option<Value>,
/// The current expiration time of the session
pub exp: Option<i64>,
}
impl Session {
@ -69,6 +72,15 @@ impl Session {
self.rt
}
/// Checks if the session has expired
pub(crate) fn expired(&self) -> bool {
match self.exp {
Some(exp) => Utc::now().timestamp() > exp,
// It is currently possible to have sessions without expiration.
None => false,
}
}
/// Convert a session into a runtime
pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> {
// Add scope auth data
@ -90,6 +102,7 @@ impl Session {
"sc".to_string() => self.sc.to_owned().into(),
"sd".to_string() => self.sd.to_owned().into(),
"tk".to_string() => self.tk.to_owned().into(),
"exp".to_string() => self.exp.to_owned().into(),
});
ctx.add_value("session", val);
// Output context
@ -132,6 +145,7 @@ impl Session {
sc: Some(sc.to_owned()),
tk: None,
sd: Some(rid),
exp: None,
}
}

View file

@ -779,6 +779,20 @@ pub enum Error {
/// The db is running without an available storage engine
#[error("The db is running without an available storage engine")]
MissingStorageEngine,
/// The session has expired either because the token used
/// to establish it has expired or because an expiration
/// was explicitly defined when establishing it
#[error("The session has expired")]
ExpiredSession,
/// The session has an invalid duration
#[error("The session has an invalid duration")]
InvalidSessionDuration,
/// The session has an invalid expiration
#[error("The session has an invalid expiration")]
InvalidSessionExpiration,
}
impl From<Error> for String {

View file

@ -129,19 +129,32 @@ pub async fn sc(
// Create the authentication key
let key = EncodingKey::from_secret(sv.code.as_ref());
// Create the authentication claim
let exp = Some(
match sv.session {
Some(v) => {
// The defined session duration must be valid
match Duration::from_std(v.0) {
// The resulting session expiration must be valid
Ok(d) => match Utc::now().checked_add_signed(d) {
Some(exp) => exp,
None => {
return Err(Error::InvalidSessionExpiration)
}
},
Err(_) => {
return Err(Error::InvalidSessionDuration)
}
}
}
_ => Utc::now() + Duration::hours(1),
}
.timestamp(),
);
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some(
match sv.session {
Some(v) => {
Utc::now() + Duration::from_std(v.0).unwrap()
}
_ => Utc::now() + Duration::hours(1),
}
.timestamp(),
),
exp,
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
sc: Some(sc.to_owned()),
@ -158,6 +171,7 @@ pub async fn sc(
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(rid.to_owned()));
session.exp = exp;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
@ -208,11 +222,12 @@ pub async fn db(
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
exp,
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
id: Some(user),
@ -226,6 +241,7 @@ pub async fn db(
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.exp = exp;
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
// Check the authentication token
match enc {
@ -259,11 +275,12 @@ pub async fn ns(
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
exp,
ns: Some(ns.to_owned()),
id: Some(user),
..Claims::default()
@ -275,6 +292,7 @@ pub async fn ns(
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.exp = exp;
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
// Check the authentication token
match enc {
@ -308,11 +326,12 @@ pub async fn root(
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let exp = Some((Utc::now() + Duration::hours(1)).timestamp());
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
exp,
id: Some(user),
..Claims::default()
};
@ -322,6 +341,7 @@ pub async fn root(
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.exp = exp;
session.au = Arc::new((&u, Level::Root).into());
// Check the authentication token
match enc {

View file

@ -127,6 +127,8 @@ pub async fn basic(
(Some(ns), Some(db)) => match verify_db_creds(kvs, ns, db, user, pass).await {
Ok(u) => {
debug!("Authenticated as database user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
Ok(())
}
@ -136,6 +138,8 @@ pub async fn basic(
(Some(ns), None) => match verify_ns_creds(kvs, ns, user, pass).await {
Ok(u) => {
debug!("Authenticated as namespace user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
Ok(())
}
@ -145,6 +149,8 @@ pub async fn basic(
(None, None) => match verify_root_creds(kvs, user, pass).await {
Ok(u) => {
debug!("Authenticated as root user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new((&u, Level::Root).into());
Ok(())
}
@ -167,16 +173,22 @@ pub async fn basic_legacy(
match verify_creds_legacy(kvs, session.ns.as_ref(), session.db.as_ref(), user, pass).await {
Ok((au, _)) if au.is_root() => {
debug!("Authenticated as root user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new(au);
Ok(())
}
Ok((au, _)) if au.is_ns() => {
debug!("Authenticated as namespace user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new(au);
Ok(())
}
Ok((au, _)) if au.is_db() => {
debug!("Authenticated as database user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.au = Arc::new(au);
Ok(())
}
@ -240,6 +252,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(),
Default::default(),
@ -274,6 +287,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(id.to_owned()));
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
Default::default(),
@ -316,6 +330,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(),
roles,
@ -348,6 +363,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
de.roles.iter().map(|r| r.into()).collect(),
@ -388,6 +404,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.exp = token_data.claims.exp;
session.au =
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns))));
Ok(())
@ -415,6 +432,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
de.roles.iter().map(|r| r.into()).collect(),
@ -443,6 +461,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
trace!("Authenticated to root level with user `{}`", id);
// Set the session
session.tk = Some(value);
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
de.roles.iter().map(|r| r.into()).collect(),
@ -597,6 +616,7 @@ mod tests {
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
@ -624,6 +644,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
// Test invalid password
@ -667,6 +688,7 @@ mod tests {
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
@ -695,6 +717,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
// Test invalid password
@ -739,6 +762,7 @@ mod tests {
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
@ -768,6 +792,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
// Test invalid password
@ -831,6 +856,7 @@ mod tests {
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -855,6 +881,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -938,6 +965,7 @@ mod tests {
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -963,6 +991,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -1049,6 +1078,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -1076,6 +1106,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//
@ -1333,6 +1364,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
let tk = match sess.tk {
Some(Value::Object(tk)) => tk,
_ => panic!("Session token is not an object"),
@ -1475,6 +1507,7 @@ mod tests {
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, claims.exp, "Session expiration is expected to match token");
}
//

View file

@ -1272,6 +1272,10 @@ impl Datastore {
sess: &Session,
vars: Variables,
) -> Result<Vec<Response>, Error> {
// Check if the session has expired
if sess.expired() {
return Err(Error::ExpiredSession);
}
// Check if anonymous actors can execute queries when auth is enabled
// TODO(sgirones): Check this as part of the authorisation layer
if self.auth_enabled && sess.au.is_anon() && !self.capabilities.allows_guest_access() {
@ -1344,6 +1348,10 @@ impl Datastore {
sess: &Session,
vars: Variables,
) -> Result<Value, Error> {
// Check if the session has expired
if sess.expired() {
return Err(Error::ExpiredSession);
}
// Check if anonymous actors can compute values when auth is enabled
// TODO(sgirones): Check this as part of the authorisation layer
if self.auth_enabled && !self.capabilities.allows_guest_access() {
@ -1423,6 +1431,10 @@ impl Datastore {
sess: &Session,
vars: Variables,
) -> Result<Value, Error> {
// Check if the session has expired
if sess.expired() {
return Err(Error::ExpiredSession);
}
// Create a new query options
let opt = Options::default()
.with_id(self.id.0)
@ -1501,6 +1513,10 @@ impl Datastore {
sess: &Session,
chn: Sender<Vec<u8>>,
) -> Result<impl Future<Output = Result<(), Error>>, Error> {
// Check if the session has expired
if sess.expired() {
return Err(Error::ExpiredSession);
}
// Retrieve the provided NS and DB
let (ns, db) = crate::iam::check::check_ns_db(sess)?;
// Create a new readonly transaction
@ -1517,6 +1533,10 @@ impl Datastore {
/// Checks the required permissions level for this session
#[instrument(level = "debug", skip(self, sess))]
pub fn check(&self, sess: &Session, action: Action, resource: Resource) -> Result<(), Error> {
// Check if the session has expired
if sess.expired() {
return Err(Error::ExpiredSession);
}
// Skip auth for Anonymous users if auth is disabled
let skip_auth = !self.is_auth_enabled() && sess.au.is_anon();
if !skip_auth {

View file

@ -502,6 +502,15 @@ impl From<Option<String>> for Value {
}
}
impl From<Option<i64>> for Value {
fn from(v: Option<i64>) -> Self {
match v {
Some(v) => Value::from(v),
None => Value::None,
}
}
}
impl From<Id> for Value {
fn from(v: Id) -> Self {
match v {

View file

@ -502,6 +502,15 @@ impl From<Option<String>> for Value {
}
}
impl From<Option<i64>> for Value {
fn from(v: Option<i64>) -> Self {
match v {
Some(v) => Value::from(v),
None => Value::None,
}
}
}
impl From<Id> for Value {
fn from(v: Id) -> Self {
match v {

View file

@ -916,3 +916,519 @@ async fn variable_auth_live_query() -> Result<(), Box<dyn std::error::Error>> {
server.finish();
Ok(())
}
#[test(tokio::test)]
async fn session_expiration() {
// Setup database server
let (addr, server) = common::start_server_with_defaults().await.unwrap();
// Connect to WebSocket
let mut socket = Socket::connect(&addr, SERVER, FORMAT).await.unwrap();
// Authenticate the connection
socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope
socket
.send_message_query(
r#"
DEFINE SCOPE scope SESSION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#,
)
.await.unwrap();
// Create resource that requires a scope session to query
socket
.send_message_query(
r#"
DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope"
;"#,
)
.await.unwrap();
socket
.send_message_query(
r#"
CREATE test:1 SET working = "yes"
;"#,
)
.await.unwrap();
// Send SIGNUP command
let res = socket
.send_request(
"signup",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "email@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Verify it returns a token
assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the token, which will expire soon
socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check if the session is now authenticated
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
// Wait two seconds for token to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Check that the session has expired and queries fail
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
// Sign in again using the same session
let res = socket
.send_request(
"signin",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "email@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Check that the session is now valid again and queries succeed
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
// Test passed
server.finish();
}
#[test(tokio::test)]
async fn session_expiration_operations() {
// Setup database server
let (addr, server) = common::start_server_with_defaults().await.unwrap();
// Connect to WebSocket
let mut socket = Socket::connect(&addr, SERVER, FORMAT).await.unwrap();
// Authenticate the connection
// We store the root token to test reauthentication later
let root_token = socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope
socket
.send_message_query(
r#"
DEFINE SCOPE scope SESSION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#,
)
.await.unwrap();
// Create resource that requires a scope session to query
socket
.send_message_query(
r#"
DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope"
;"#,
)
.await.unwrap();
socket
.send_message_query(
r#"
CREATE test:1 SET working = "yes"
;"#,
)
.await.unwrap();
// Send SIGNUP command
let res = socket
.send_request(
"signup",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "email@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Verify it returns a token
assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the token, which will expire soon
socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check if the session is now authenticated
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
// Wait two seconds for the session to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Check if the session is now expired
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
// Test operations that SHOULD NOT work with an expired session
let operations_ko = vec![
socket.send_request("let", json!(["let_var", "let_value",])),
socket.send_request("set", json!(["set_var", "set_value",])),
socket.send_request("info", json!([])),
socket.send_request("select", json!(["tester",])),
socket
.send_request(
"insert",
json!([
"tester",
{
"name": "foo",
"value": "bar",
}
]),
),
socket
.send_request(
"create",
json!([
"tester",
{
"value": "bar",
}
]),
),
socket
.send_request(
"update",
json!([
"tester",
{
"value": "bar",
}
]),
),
socket
.send_request(
"merge",
json!([
"tester",
{
"value": "bar",
}
]),
),
socket
.send_request(
"patch",
json!([
"tester:id",
[
{
"op": "add",
"path": "value",
"value": "bar"
},
{
"op": "remove",
"path": "name",
}
]
]),
),
socket.send_request("delete", json!(["tester"])),
socket.send_request("live", json!(["tester"])),
socket.send_request("kill", json!(["tester"])),
];
// Futures are executed sequentially as some operations rely on the previous state
for operation in operations_ko {
let res = operation.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
};
// Test operations that SHOULD work with an expired session
let operations_ok = vec![
socket.send_request("use", json!([NS, DB])),
socket.send_request("ping", json!([])),
socket.send_request("version", json!([])),
socket.send_request("invalidate", json!([])),
];
// Futures are executed sequentially as some operations rely on the previous state
for operation in operations_ok {
let res = operation.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
};
// Test operations that SHOULD work with an expired session
// These operations will refresh the session expiration
let res = socket
.send_request(
"signup",
json!([{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "another@email.com",
"pass": "pass",
}]),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Wait two seconds for the session to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// The session must be expired now or we fail the test
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
let res = socket
.send_request(
"signin",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "another@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Wait two seconds for the session to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// The session must be expired now or we fail the test
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
// This needs to be last operation as the session will no longer expire afterwards
let res = socket.send_request("authenticate", json!([root_token,])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Test passed
server.finish();
}
#[test(tokio::test)]
async fn session_reauthentication() {
// Setup database server
let (addr, server) = common::start_server_with_defaults().await.unwrap();
// Connect to WebSocket
let mut socket = Socket::connect(&addr, SERVER, FORMAT).await.unwrap();
// Authenticate the connection and store the root level token
let root_token = socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Check that we have root access
socket.send_message_query("INFO FOR ROOT").await.unwrap();
// Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope
socket
.send_message_query(
r#"
DEFINE SCOPE scope SESSION 1h
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#,
)
.await.unwrap();
// Create resource that requires a scope session to query
socket
.send_message_query(
r#"
DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope"
;"#,
)
.await.unwrap();
socket
.send_message_query(
r#"
CREATE test:1 SET working = "yes"
;"#,
)
.await.unwrap();
// Send SIGNUP command
let res = socket
.send_request(
"signup",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "email@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Verify it returns a token
assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the scope token
socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check that we do not have root access
let res = socket.send_message_query("INFO FOR ROOT").await.unwrap();
assert_eq!(res[0]["status"], "ERR", "result: {:?}", res);
assert_eq!(
res[0]["result"], "IAM error: Not enough permissions to perform this action",
"result: {:?}",
res
);
// Check if the session is authenticated for the scope
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
// Authenticate using the root token
socket.send_request("authenticate", json!([root_token,])).await.unwrap();
// Check that we have root access again
let res = socket.send_message_query("INFO FOR ROOT").await.unwrap();
assert_eq!(res[0]["status"], "OK", "result: {:?}", res);
// Test passed
server.finish();
}
#[test(tokio::test)]
async fn session_reauthentication_expired() {
// Setup database server
let (addr, server) = common::start_server_with_defaults().await.unwrap();
// Connect to WebSocket
let mut socket = Socket::connect(&addr, SERVER, FORMAT).await.unwrap();
// Authenticate the connection and store the root level token
let root_token = socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Check that we have root access
socket.send_message_query("INFO FOR ROOT").await.unwrap();
// Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope
socket
.send_message_query(
r#"
DEFINE SCOPE scope SESSION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#,
)
.await.unwrap();
// Create resource that requires a scope session to query
socket
.send_message_query(
r#"
DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope"
;"#,
)
.await.unwrap();
socket
.send_message_query(
r#"
CREATE test:1 SET working = "yes"
;"#,
)
.await.unwrap();
// Send SIGNUP command
let res = socket
.send_request(
"signup",
json!(
[{
"ns": NS,
"db": DB,
"sc": "scope",
"email": "email@email.com",
"pass": "pass",
}]
),
)
.await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
// Verify response contains no error
assert!(res.keys().all(|k| ["id", "result"].contains(&k.as_str())), "result: {:?}", res);
// Verify it returns a token
assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the scope token, which will expire soon
socket.send_request("authenticate", json!([res,])).await.unwrap();
// Wait two seconds for token to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Verify that the session has expired
let res = socket.send_request("query", json!(["SELECT VALUE working FROM test:1",])).await;
assert!(res.is_ok(), "result: {:?}", res);
let res = res.unwrap();
assert!(res.is_object(), "result: {:?}", res);
let res = res.as_object().unwrap();
assert_eq!(res["error"], json!({"code": -32000, "message": "There was a problem with the database: The session has expired"}));
// Authenticate using the root token, which has not expired yet
socket.send_request("authenticate", json!([root_token,])).await.unwrap();
// Check that we have root access and the session is not expired
let res = socket.send_message_query("INFO FOR ROOT").await.unwrap();
assert_eq!(res[0]["status"], "OK", "result: {:?}", res);
// Test passed
server.finish();
}