Implement session expiration (#3561)
This commit is contained in:
parent
e5c63234ca
commit
957eff19a9
8 changed files with 647 additions and 12 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue