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