diff --git a/core/src/iam/signin.rs b/core/src/iam/signin.rs index b426066a..ef7eafb0 100644 --- a/core/src/iam/signin.rs +++ b/core/src/iam/signin.rs @@ -780,6 +780,14 @@ mod tests { use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use std::collections::HashMap; + struct TestLevel { + level: &'static str, + ns: Option<&'static str>, + db: Option<&'static str>, + } + + const AVAILABLE_ROLES: [Role; 3] = [Role::Viewer, Role::Editor, Role::Owner]; + #[tokio::test] async fn test_signin_record() { // Test with correct credentials @@ -1061,567 +1069,218 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ== } #[tokio::test] - async fn test_signin_db_user() { - // - // Test without roles or 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'", &sess, None).await.unwrap(); - - // Signin with the user - let mut sess = Session { - ns: Some("test".to_string()), - db: 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.ns, Some("test".to_string())); - assert_eq!(sess.db, 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, "Default system user expiration is expected to be None"); + async fn test_signin_user() { + #[derive(Debug)] + struct TestCase { + title: &'static str, + password: &'static str, + roles: Vec, + token_expiration: Option, + session_expiration: Option, + expect_ok: bool, } - // - // 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' DURATION FOR TOKEN 365d, FOR SESSION NONE", - &sess, - None, - ) - .await - .unwrap(); + let test_cases = vec![ + TestCase { + title: "without roles or expiration", + password: "pass", + roles: vec![Role::Viewer], + token_expiration: None, + session_expiration: None, + expect_ok: true, + }, + TestCase { + title: "with roles and expiration", + password: "pass", + roles: vec![Role::Editor, Role::Owner], + token_expiration: Some(Duration::days(365)), + session_expiration: Some(Duration::days(1)), + expect_ok: true, + }, + TestCase { + title: "with invalid password", + password: "invalid", + roles: vec![], + token_expiration: None, + session_expiration: None, + expect_ok: false, + }, + ]; - // 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; + let test_levels = vec![ + TestLevel { + level: "ROOT", + ns: None, + db: None, + }, + TestLevel { + level: "NS", + ns: Some("test"), + db: None, + }, + TestLevel { + level: "DB", + ns: Some("test"), + db: Some("test"), + }, + ]; - 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(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"), + for level in &test_levels { + for case in &test_cases { + println!("Test case: {} level {}", level.level, case.title); + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + + let roles_clause = if case.roles.is_empty() { + String::new() + } else { + let roles: Vec<&str> = case + .roles + .iter() + .map(|r| match r { + Role::Viewer => "VIEWER", + Role::Editor => "EDITOR", + Role::Owner => "OWNER", + }) + .collect(); + format!("ROLES {}", roles.join(", ")) }; - 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" + + let mut duration_clause = String::new(); + if case.token_expiration.is_some() || case.session_expiration.is_some() { + duration_clause = format!("DURATION") + } + if let Some(duration) = case.token_expiration { + duration_clause = + format!("{} FOR TOKEN {}s", duration_clause, duration.num_seconds()) + } + if let Some(duration) = case.session_expiration { + duration_clause = + format!("{} FOR SESSION {}s", duration_clause, duration.num_seconds()) + } + + let define_user_query = format!( + "DEFINE USER user ON {} PASSWORD 'pass' {} {}", + level.level, roles_clause, duration_clause, ); - // 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(); + ds.execute(&define_user_query, &sess, None).await.unwrap(); - // Signin with the user - let mut sess = Session { - ns: Some("test".to_string()), - db: 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.ns, Some("test".to_string())); - assert_eq!(sess.db, 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 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 - 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(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 mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() }; - 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 - { - 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'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = db_user( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "user".to_string(), - "invalid".to_string(), - ) - .await; - - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); - } - } - - #[tokio::test] - async fn test_signin_ns_user() { - // - // Test without roles or 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'", &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, "Default system user expiration is expected to be None"); - } - - // - // 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' 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(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 res = match level.level { + "ROOT" => { + root_user(&ds, &mut sess, "user".to_string(), case.password.to_string()) + .await + } + "NS" => { + ns_user( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "user".to_string(), + case.password.to_string(), + ) + .await + } + "DB" => { + db_user( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "user".to_string(), + case.password.to_string(), + ) + .await + } + _ => panic!("Unsupported level"), }; - 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") + + if case.expect_ok { + assert!(res.is_ok(), "Failed to signin: {:?}", res); + assert_eq!(sess.ns, level.ns.map(|s| s.to_string())); + assert_eq!(sess.db, level.db.map(|s| s.to_string())); + assert_eq!(sess.au.level().ns(), level.ns); + assert_eq!(sess.au.level().db(), level.db); + assert_eq!(sess.au.id(), "user"); + + // Check auth level + match level.level { + "ROOT" => assert!(sess.au.is_root()), + "NS" => assert!(sess.au.is_ns()), + "DB" => assert!(sess.au.is_db()), + _ => panic!("Unsupported level"), + } + + // Check roles + for role in &AVAILABLE_ROLES { + let has_role = sess.au.has_role(role); + let should_have_role = case.roles.contains(role); + assert_eq!(has_role, should_have_role, "Role {:?} check failed", role); + } + + // Check session expiration + if let Some(exp_duration) = case.session_expiration { + let exp = sess.exp.unwrap(); + let min_exp = + (Utc::now() + exp_duration - Duration::seconds(10)).timestamp(); + let max_exp = + (Utc::now() + exp_duration + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to match the defined duration" + ); + } else { + assert_eq!(sess.exp, None, "Session expiration is expected to be None"); + } + + // Check issued token + if let Ok(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 session expiration + if let Some(exp_duration) = case.token_expiration { + let exp = match token_data.claims.exp { + Some(exp) => exp, + _ => panic!("Token is missing expiration claim"), + }; + let min_exp = + (Utc::now() + exp_duration - Duration::seconds(10)).timestamp(); + let max_exp = + (Utc::now() + exp_duration + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to match the defined duration" + ); + } else { + assert_eq!(sess.exp, None, "Session expiration is expected to be None"); + } + + // Check required token claims + assert_eq!(token_data.claims.ns, level.ns.map(|s| s.to_string())); + assert_eq!(token_data.claims.db, level.db.map(|s| s.to_string())); + assert_eq!(token_data.claims.id, Some("user".to_string())); + } else { + panic!("Token could not be extracted from result") + } + } else { + assert!(res.is_err(), "Unexpected successful signin: {:?}", res); + } } } - - // - // 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(); - - // 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 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 - 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(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 - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - ds.execute("DEFINE USER user ON NS PASSWORD 'pass'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = ns_user( - &ds, - &mut sess, - "test".to_string(), - "user".to_string(), - "invalid".to_string(), - ) - .await; - - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); - } - } - - #[tokio::test] - async fn test_signin_root_user() { - // - // Test without roles or expiration defined - // - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass'", &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, "Default system user expiration is expected to be None"); - } - - // - // 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(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 DURATION FOR TOKEN 15m, FOR SESSION 6h", &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 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 - 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(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 - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = root_user(&ds, &mut sess, "user".to_string(), "invalid".to_string()).await; - - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); - } } #[tokio::test] @@ -1896,2038 +1555,1016 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ== } #[tokio::test] - async fn test_signin_bearer_for_user_db() { - // Test with correct bearer key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + async fn test_signin_bearer_for_user() { + let test_levels = vec![ + TestLevel { + level: "ROOT", + ns: None, + db: None, + }, + TestLevel { + level: "NS", + ns: Some("test"), + db: None, + }, + TestLevel { + level: "DB", + ns: Some("test"), + db: Some("test"), + }, + ]; - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + for level in &test_levels { + println!("Test level: {}", level.level); - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; + // Test with correct bearer key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert!(sess.au.is_db()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), Some("test")); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); - // Test with correct bearer key and AUTHENTICATE clause succeeding - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER - AUTHENTICATE {{ - RETURN NONE - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + assert!(res.is_ok(), "Failed to sign in with bearer key: {:?}", res); + assert_eq!(sess.ns, level.ns.map(|s| s.to_string())); + assert_eq!(sess.db, level.db.map(|s| s.to_string())); - // Signin with the bearer key - let mut sess = Session { - db: Some("test".to_string()), - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert!(sess.au.is_db()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), Some("test")); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } - - // Test with correct bearer key and AUTHENTICATE clause failing - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER - AUTHENTICATE {{ - THROW "Test authentication error"; - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - db: Some("test".to_string()), - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::Thrown(e)) => { - assert_eq!(e, "Test authentication error") + // Check auth level + match level.level { + "ROOT" => assert!(sess.au.is_root()), + "NS" => assert!(sess.au.is_ns()), + "DB" => assert!(sess.au.is_db()), + _ => panic!("Unsupported level"), } - res => panic!( - "Expected a thrown authentication error, but instead received: {:?}", - res - ), + assert_eq!(sess.au.level().ns(), level.ns); + assert_eq!(sess.au.level().db(), level.db); + + // Check roles + assert!( + !sess.au.has_role(&Role::Viewer), + "Auth user expected to not have Viewer role" + ); + assert!( + // User is defined with this role only + sess.au.has_role(&Role::Editor), + "Auth user expected to have Editor role" + ); + assert!( + !sess.au.has_role(&Role::Owner), + "Auth user expected to not have Owner role" + ); + + // Check expiration + let exp = sess.exp.unwrap(); + 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 match the defined duration", + ); } - } - // Test with expired grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with correct bearer key and AUTHENTICATE clause succeeding + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER + AUTHENTICATE {{ + RETURN NONE + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); - // Wait for the grant to expire - std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; + assert!(res.is_ok(), "Failed to sign in with bearer key: {:?}", res); + assert_eq!(sess.ns, level.ns.map(|s| s.to_string())); + assert_eq!(sess.db, level.db.map(|s| s.to_string())); - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with revoked grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Get grant identifier from key - let kid = key.split("-").collect::>()[2]; - - // Revoke grant - ds.execute( - &format!( - r#" - ACCESS api REVOKE `{kid}`; - "# - ), - &sess, - None, - ) - .await - .unwrap(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with removed access method - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Remove bearer access method - ds.execute("REMOVE ACCESS api ON DATABASE", &sess, None).await.unwrap(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::AccessNotFound) => {} // ok - res => panic!( - "Expected an access method not found error, but instead received: {:?}", - res - ), - } - } - - // Test with missing key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let _key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - - // The key parameter is not inserted: - let vars: HashMap<&str, Value> = HashMap::new(); - // vars.insert("key", key.into()); - - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::AccessBearerMissingKey) => {} // ok - res => panic!( - "Expected a missing key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key prefix part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key prefix - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key length - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Remove a character from the bearer key - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key identifier part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key identifier - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key value - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON DATABASE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key value - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key - [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = db_access( - &ds, - &mut sess, - "test".to_string(), - "test".to_string(), - "api".to_string(), - vars.into(), - ) - .await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - } - - #[tokio::test] - async fn test_signin_bearer_for_user_ns() { - // Test with correct bearer key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } - - // Test with correct bearer key and AUTHENTICATE clause succeeding - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER - AUTHENTICATE {{ - RETURN NONE - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } - - // Test with correct bearer key and AUTHENTICATE clause failing - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER - AUTHENTICATE {{ - THROW "Test authentication error"; - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::Thrown(e)) => { - assert_eq!(e, "Test authentication error") + // Check auth level + match level.level { + "ROOT" => assert!(sess.au.is_root()), + "NS" => assert!(sess.au.is_ns()), + "DB" => assert!(sess.au.is_db()), + _ => panic!("Unsupported level"), } - res => panic!( - "Expected a thrown authentication error, but instead received: {:?}", - res - ), + assert_eq!(sess.au.level().ns(), level.ns); + assert_eq!(sess.au.level().db(), level.db); + + // Check roles + assert!( + !sess.au.has_role(&Role::Viewer), + "Auth user expected to not have Viewer role" + ); + assert!( + // User is defined with this role only + sess.au.has_role(&Role::Editor), + "Auth user expected to have Editor role" + ); + assert!( + !sess.au.has_role(&Role::Owner), + "Auth user expected to not have Owner role" + ); + + // Check expiration + let exp = sess.exp.unwrap(); + 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 match the defined duration", + ); } - } - // Test with expired grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with correct bearer key and AUTHENTICATE clause failing + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER + AUTHENTICATE {{ + THROW "Test authentication error"; + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level, + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); - // Wait for the grant to expire - std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with revoked grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Get grant identifier from key - let kid = key.split("-").collect::>()[2]; - - // Revoke grant - ds.execute( - &format!( - r#" - ACCESS api REVOKE `{kid}`; - "# - ), - &sess, - None, - ) - .await - .unwrap(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with removed access method - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Remove bearer access method - ds.execute("REMOVE ACCESS api ON NAMESPACE", &sess, None).await.unwrap(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::AccessNotFound) => {} // ok - res => panic!( - "Expected an access method not found error, but instead received: {:?}", - res - ), - } - } - - // Test with missing key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let _key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - - // The key parameter is not inserted: - let vars: HashMap<&str, Value> = HashMap::new(); - // vars.insert("key", key.into()); - - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::AccessBearerMissingKey) => {} // ok - res => panic!( - "Expected a missing key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key prefix part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key prefix - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key length - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Remove a character from the bearer key - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key identifier part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key identifier - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key value - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON NAMESPACE ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key value - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key - [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = - ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - } - - #[tokio::test] - async fn test_signin_bearer_for_user_root() { - // Test with correct bearer key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } - - // Test with correct bearer key and AUTHENTICATE clause succeeding - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER - AUTHENTICATE {{ - RETURN NONE - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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 not have Owner role"); - // 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(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 the defined duration" - ); - } - - // Test with correct bearer key and AUTHENTICATE clause failing - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER - AUTHENTICATE {{ - THROW "Test authentication error"; - }} - DURATION FOR SESSION 2h - ; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); - - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - match res { - Err(Error::Thrown(e)) => { - assert_eq!(e, "Test authentication error") + match res { + Err(Error::Thrown(e)) => { + assert_eq!(e, "Test authentication error") + } + res => panic!( + "Expected a thrown authentication error, but instead received: {:?}", + res + ), } - res => panic!( - "Expected a thrown authentication error, but instead received: {:?}", - res - ), } - } - // Test with expired grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, + // Test with expired grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Wait for the grant to expire + std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with revoked grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Get grant identifier from key + let kid = key.split("-").collect::>()[2]; + + // Revoke grant + ds.execute( + &format!( + r#" + ACCESS api ON {} REVOKE `{kid}`; + "#, + level.level + ), &sess, None, ) .await .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - // Wait for the grant to expire - std::thread::sleep(Duration::seconds(2).to_std().unwrap()); - - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } } - } - // Test with revoked grant - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with removed access method + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); - // Get grant identifier from key - let kid = key.split("-").collect::>()[2]; + // Remove bearer access method + ds.execute(format!("REMOVE ACCESS api ON {}", level.level).as_str(), &sess, None) + .await + .unwrap(); - // Revoke grant - ds.execute( - &format!( - r#" - ACCESS api REVOKE `{kid}`; - "# - ), - &sess, - None, - ) - .await - .unwrap(); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::AccessNotFound) => {} // ok + res => panic!( + "Expected an access method not found error, but instead received: {:?}", + res + ), + } } - } - // Test with removed access method - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with missing key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let _key = grant.get("key").unwrap().clone().as_string(); - // Remove bearer access method - ds.execute("REMOVE ACCESS api ON ROOT", &sess, None).await.unwrap(); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + // The key parameter is not inserted: + let vars: HashMap<&str, Value> = HashMap::new(); + // vars.insert("key", key.into()); - match res { - Err(Error::AccessNotFound) => {} // ok - res => panic!( - "Expected an access method not found error, but instead received: {:?}", - res - ), + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; + + match res { + Err(Error::AccessBearerMissingKey) => {} // ok + res => panic!( + "Expected a missing key authentication error, but instead received: {:?}", + res + ), + } } - } - // Test with missing key - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with incorrect bearer key prefix part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let _key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; + // Replace a character from the key prefix + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; + let key: String = invalid_key.into_iter().collect(); - // The key parameter is not inserted: - let vars: HashMap<&str, Value> = HashMap::new(); - // vars.insert("key", key.into()); + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - match res { - Err(Error::AccessBearerMissingKey) => {} // ok - res => panic!( - "Expected a missing key authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } } - } - // Test with incorrect bearer key prefix part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with incorrect bearer key length + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); - // Replace a character from the key prefix - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; - let key: String = invalid_key.into_iter().collect(); + // Remove a character from the bearer key + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); + let key: String = invalid_key.into_iter().collect(); - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } } - } - // Test with incorrect bearer key length - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with incorrect bearer key identifier part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); - // Remove a character from the bearer key - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); - let key: String = invalid_key.into_iter().collect(); + // Replace a character from the key identifier + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - match res { - Err(Error::AccessGrantBearerInvalid) => {} // ok - res => panic!( - "Expected an invalid key authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } } - } - // Test with incorrect bearer key identifier part - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); + // Test with incorrect bearer key value + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + &format!( + r#" + DEFINE ACCESS api ON {} TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON {} ROLES EDITOR; + ACCESS api ON {} GRANT FOR USER tobie; + "#, + level.level, level.level, level.level + ), + &sess, + None, + ) + .await + .unwrap(); - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); + // Get the bearer key from grant + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); - // Replace a character from the key identifier - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); + // Replace a character from the key value + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key + [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + // Sign in with the bearer key + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = match level.level { + "DB" => { + db_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + level.db.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "NS" => { + ns_access( + &ds, + &mut sess, + level.ns.unwrap().to_string(), + "api".to_string(), + vars.into(), + ) + .await + } + "ROOT" => root_access(&ds, &mut sess, "api".to_string(), vars.into()).await, + _ => panic!("Unsupported level"), + }; - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - - // Test with incorrect bearer key value - { - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - let res = ds - .execute( - r#" - DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; - DEFINE USER tobie ON ROOT ROLES EDITOR; - ACCESS api GRANT FOR USER tobie; - "#, - &sess, - None, - ) - .await - .unwrap(); - - // Get the bearer key from grant. - let result = if let Ok(res) = &res.last().unwrap().result { - res.clone() - } else { - panic!("Unable to retrieve bearer key grant"); - }; - let grant = result - .coerce_to_object() - .unwrap() - .get("grant") - .unwrap() - .clone() - .coerce_to_object() - .unwrap(); - let valid_key = grant.get("key").unwrap().clone().as_string(); - - // Replace a character from the key value - let mut invalid_key: Vec = valid_key.chars().collect(); - invalid_key - [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; - let key: String = invalid_key.into_iter().collect(); - - // Signin with the bearer key - let mut sess = Session { - ..Default::default() - }; - let mut vars: HashMap<&str, Value> = HashMap::new(); - vars.insert("key", key.into()); - let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } } } } diff --git a/core/src/iam/verify.rs b/core/src/iam/verify.rs index 57c96f2d..eb04243c 100644 --- a/core/src/iam/verify.rs +++ b/core/src/iam/verify.rs @@ -699,265 +699,204 @@ mod tests { use chrono::Duration; use jsonwebtoken::{encode, EncodingKey}; + struct TestLevel { + level: &'static str, + ns: Option<&'static str>, + db: Option<&'static str>, + } + + const AVAILABLE_ROLES: [Role; 3] = [Role::Viewer, Role::Editor, Role::Owner]; + #[tokio::test] - async fn test_basic_root() { - // - // Test without roles or 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 ROOT PASSWORD 'pass'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", None, None).await; - - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "user"); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role"); - assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); - assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); - assert_eq!(sess.exp, None, "Default system user expiration is expected to be None"); + async fn test_basic() { + #[derive(Debug)] + struct TestCase { + title: &'static str, + password: &'static str, + roles: Vec, + expiration: Option, + expect_ok: bool, } - // - // 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 ROOT PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR SESSION 1d", - &sess, - None, - ) - .await - .unwrap(); + let test_cases = vec![ + TestCase { + title: "without roles or expiration", + password: "pass", + roles: vec![Role::Viewer], + expiration: None, + expect_ok: true, + }, + TestCase { + title: "with roles and expiration", + password: "pass", + roles: vec![Role::Editor, Role::Owner], + expiration: Some(Duration::days(1)), + expect_ok: true, + }, + TestCase { + title: "with invalid password", + password: "invalid", + roles: vec![], + expiration: None, + expect_ok: false, + }, + ]; - let mut sess = Session { - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", None, None).await; + let test_levels = vec![ + TestLevel { + level: "ROOT", + ns: None, + db: None, + }, + TestLevel { + level: "NS", + ns: Some("test"), + db: None, + }, + TestLevel { + level: "DB", + ns: Some("test"), + db: Some("test"), + }, + ]; - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "user"); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - 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"); - // 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(); - let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp(); - assert!( - exp > min_exp && exp < max_exp, - "Session expiration is expected to match the defined duration" - ); - } + for level in &test_levels { + for case in &test_cases { + println!("Test case: {} level {}", level.level, case.title); + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); - // Test invalid password - { - 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'", &sess, None).await.unwrap(); + let roles_clause = if case.roles.is_empty() { + String::new() + } else { + let roles: Vec<&str> = case + .roles + .iter() + .map(|r| match r { + Role::Viewer => "VIEWER", + Role::Editor => "EDITOR", + Role::Owner => "OWNER", + }) + .collect(); + format!("ROLES {}", roles.join(", ")) + }; - let mut sess = Session { - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "invalid", None, None).await; + let duration_clause = if let Some(duration) = case.expiration { + format!("DURATION FOR SESSION {}s", duration.num_seconds()) + } else { + String::new() + }; - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); + let define_user_query = format!( + "DEFINE USER user ON {} PASSWORD 'pass' {} {}", + level.level, roles_clause, duration_clause, + ); + + ds.execute(&define_user_query, &sess, None).await.unwrap(); + + let mut sess = Session { + ns: level.ns.map(String::from), + db: level.db.map(String::from), + ..Default::default() + }; + + let res = basic(&ds, &mut sess, "user", case.password, level.ns, level.db).await; + + if case.expect_ok { + assert!(res.is_ok(), "Failed to signin: {:?}", res); + assert_eq!(sess.au.id(), "user"); + + // Check auth level + assert_eq!(sess.au.level().ns(), level.ns); + assert_eq!(sess.au.level().db(), level.db); + match level.level { + "ROOT" => assert!(sess.au.is_root()), + "NS" => assert!(sess.au.is_ns()), + "DB" => assert!(sess.au.is_db()), + _ => panic!("Unsupported level"), + } + + // Check roles + for role in &AVAILABLE_ROLES { + let has_role = sess.au.has_role(role); + let should_have_role = case.roles.contains(role); + assert_eq!(has_role, should_have_role, "Role {:?} check failed", role); + } + + // Check expiration + if let Some(exp_duration) = case.expiration { + let exp = sess.exp.unwrap(); + let min_exp = + (Utc::now() + exp_duration - Duration::seconds(10)).timestamp(); + let max_exp = + (Utc::now() + exp_duration + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to match the defined duration" + ); + } else { + assert_eq!(sess.exp, None, "Expiration is expected to be None"); + } + } else { + assert!(res.is_err(), "Unexpected successful signin: {:?}", res); + } + } } } #[tokio::test] - async fn test_basic_ns() { - // - // Test without roles or 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 NS PASSWORD 'pass'", &sess, None).await.unwrap(); - - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", Some("test"), None).await; - - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "user"); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role"); - assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); - assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); - assert_eq!(sess.exp, None, "Default system user expiration is expected to be None"); + async fn test_token() { + #[derive(Debug)] + struct TestCase { + title: &'static str, + roles: Option>, + key: &'static str, + expect_roles: Vec, + expect_error: bool, } - // - // 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 NS PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR SESSION 1d", - &sess, - None, - ) - .await - .unwrap(); + let test_cases = vec![ + TestCase { + title: "with no roles", + roles: None, + key: "secret", + expect_roles: vec![Role::Viewer], + expect_error: false, + }, + TestCase { + title: "with roles", + roles: Some(vec!["editor", "owner"]), + key: "secret", + expect_roles: vec![Role::Editor, Role::Owner], + expect_error: false, + }, + TestCase { + title: "with invalid token signature", + roles: None, + key: "invalid", + expect_roles: vec![], + expect_error: true, + }, + ]; - let mut sess = Session { - ns: Some("test".to_string()), - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", Some("test"), None).await; + let test_levels = vec![ + TestLevel { + level: "ROOT", + ns: None, + db: None, + }, + TestLevel { + level: "NS", + ns: Some("test"), + db: None, + }, + TestLevel { + level: "DB", + ns: Some("test"), + db: Some("test"), + }, + ]; - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "user"); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - 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 - 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(); - let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp(); - assert!( - exp > min_exp && exp < max_exp, - "Session expiration is expected to match the defined duration" - ); - } - - // Test invalid password - { - 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'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "invalid", Some("test"), None).await; - - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); - } - } - - #[tokio::test] - async fn test_basic_db() { - // - // Test without roles or 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'", &sess, None).await.unwrap(); - - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", Some("test"), Some("test")).await; - - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, 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, "Default system user expiration is expected to be None"); - } - - // - // 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 SESSION 1d", - &sess, - None, - ) - .await - .unwrap(); - - let mut sess = Session { - ns: Some("test".to_string()), - db: Some("test".to_string()), - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "pass", Some("test"), Some("test")).await; - - assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, 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 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 - 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(); - let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp(); - assert!( - exp > min_exp && exp < max_exp, - "Session expiration is expected to match the defined duration" - ); - } - - // Test invalid password - { - 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'", &sess, None).await.unwrap(); - - let mut sess = Session { - ..Default::default() - }; - let res = basic(&ds, &mut sess, "user", "invalid", Some("test"), Some("test")).await; - - assert!(res.is_err(), "Unexpected successful signin: {:?}", res); - } - } - - #[tokio::test] - async fn test_token_root() { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); let claims = Claims { iss: Some("surrealdb-test".to_string()), iat: Some(Utc::now().timestamp()), @@ -969,349 +908,157 @@ 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 ROOT TYPE JWT ALGORITHM HS512 KEY '{secret}' DURATION FOR SESSION 30d").as_str(), - &sess, - None, - ) - .await - .unwrap(); - // - // Test without roles defined - // - { - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "token"); - 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"); - // 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" - ); - } - - // - // Test with roles defined - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_root()); - 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"); - // 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" - ); - } - - // - // Test with invalid signature - // - { - // Prepare the claims object - let claims = claims.clone(); - // Create the token - let key = EncodingKey::from_secret("invalid".as_ref()); - 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 token: {:?}", res); - } - } - - #[tokio::test] - async fn test_token_ns() { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("token".to_string()), - ns: 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 NS TYPE JWT ALGORITHM HS512 KEY '{secret}' DURATION FOR SESSION 30d").as_str(), - &sess, - None, - ) - .await - .unwrap(); - - // - // Test without roles defined - // - { - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "token"); - 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"); - // 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" - ); - } - - // - // Test with roles defined - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - 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"); - // 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" - ); - } - - // - // Test with invalid signature - // - { - // Prepare the claims object - let claims = claims.clone(); - // Create the token - let key = EncodingKey::from_secret("invalid".as_ref()); - 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 token: {:?}", res); - } - - // - // Test with valid token invalid ns - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.ns = Some("invalid".to_string()); - // 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 token: {:?}", res); - } - } - - #[tokio::test] - async fn test_token_db() { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().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 level in &test_levels { + // Define the access token for that level + ds.execute( + format!( + r#" + DEFINE ACCESS token ON {} TYPE JWT + ALGORITHM HS512 KEY 'secret' DURATION FOR SESSION 30d + ; + "#, + level.level + ) .as_str(), - &sess, - None, - ) - .await - .unwrap(); + &sess, + None, + ) + .await + .unwrap(); - // - // Test without roles defined - // - { - // 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; + for case in &test_cases { + println!("Test case: {} level {}", level.level, case.title); - assert!(res.is_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - 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"); - // 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" - ); - } + // Prepare the claims object + let mut claims = claims.clone(); + claims.ns = level.ns.map(|s| s.to_string()); + claims.db = level.db.map(|s| s.to_string()); + claims.roles = + case.roles.clone().map(|roles| roles.into_iter().map(String::from).collect()); - // - // Test with roles defined - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]); - // 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; + // Create the token + let key = EncodingKey::from_secret(case.key.as_ref()); + let enc = encode(&HEADER, &claims, &key).unwrap(); - assert!(res.is_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - 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 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"); - // 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" - ); - } + // Authenticate with the token + let mut sess = Session::default(); + let res = token(&ds, &mut sess, &enc).await; - // - // Test with invalid signature - // - { - // Prepare the claims object - let claims = claims.clone(); - // Create the token - let key = EncodingKey::from_secret("invalid".as_ref()); - let enc = encode(&HEADER, &claims, &key).unwrap(); - // Signin with the token - let mut sess = Session::default(); - let res = token(&ds, &mut sess, &enc).await; + if case.expect_error { + assert!(res.is_err(), "Unexpected success for case: {:?}", case); + } else { + assert!(res.is_ok(), "Failed to sign in with token for case: {:?}", case); + assert_eq!(sess.ns, level.ns.map(|s| s.to_string())); + assert_eq!(sess.db, level.db.map(|s| s.to_string())); + assert_eq!(sess.au.id(), "token"); - assert!(res.is_err(), "Unexpected success signing in with token: {:?}", res); - } + // Check roles + for role in &AVAILABLE_ROLES { + let has_role = sess.au.has_role(role); + let should_have_role = case.expect_roles.contains(role); + assert_eq!(has_role, should_have_role, "Role {:?} check failed", role); + } - // - // Test with valid token invalid db - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.db = Some("invalid".to_string()); - // 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 token: {:?}", res); + // Ensure that the expiration is set correctly + let exp = sess.exp.unwrap(); + 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 in case: {:?}", + case + ); + } + } } } #[tokio::test] - async fn test_token_db_record() { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); + async fn test_token_record() { + #[derive(Debug)] + struct TestCase { + title: &'static str, + ids: Vec<&'static str>, + roles: Option>, + key: &'static str, + expect_error: bool, + } + + let test_cases = vec![ + TestCase { + title: "with no roles", + ids: vec!["user:test"], + roles: None, + key: "secret", + expect_error: false, + }, + TestCase { + title: "with roles", + ids: vec!["user:test"], + roles: Some(vec!["editor", "owner"]), + key: "secret", + expect_error: false, + }, + TestCase { + title: "with invalid token signature", + ids: vec!["user:test"], + roles: None, + key: "invalid", + expect_error: true, + }, + TestCase { + title: "with invalid id", + ids: vec!["invalid"], + roles: None, + key: "invalid", + expect_error: true, + }, + TestCase { + title: "with generic id", + ids: vec!["user:2k9qnabxuxh8k4d5gfto"], + roles: None, + key: "secret", + expect_error: false, + }, + TestCase { + title: "with numeric ids", + ids: vec!["user:1", "user:2", "user:100", "user:10000000"], + roles: None, + key: "secret", + expect_error: false, + }, + TestCase { + title: "with alphanumeric ids", + ids: vec!["user:username", "user:username1", "user:username10", "user:username100"], + roles: None, + key: "secret", + expect_error: false, + }, + TestCase { + title: "with ids including special characters", + ids: vec![ + "user:⟨user.name⟩", + "user:⟨user.name1⟩", + "user:⟨user.name10⟩", + "user:⟨user.name100⟩", + ], + roles: None, + key: "secret", + expect_error: false, + }, + TestCase { + title: "with UUID ids", + ids: vec!["user:⟨83149446-95f5-4c0d-9f42-136e7b272456⟩"], + roles: None, + key: "secret", + expect_error: false, + }, + ]; + + let secret = "secret"; let claims = Claims { iss: Some("surrealdb-test".to_string()), iat: Some(Utc::now().timestamp()), @@ -1320,7 +1067,6 @@ mod tests { ns: Some("test".to_string()), db: Some("test".to_string()), ac: Some("token".to_string()), - id: Some("user:test".to_string()), ..Claims::default() }; @@ -1343,261 +1089,60 @@ mod tests { .await .unwrap(); - // - // Test without roles defined - // Roles should be ignored in record access - // - { - // 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; + for case in &test_cases { + println!("Test case: {}", case.title); - assert!(res.is_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), "user:test"); - assert!(sess.au.is_record()); - 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 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 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" - ); - } - - // - // Test with roles defined - // Roles should be ignored in record access - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), "user:test"); - assert!(sess.au.is_record()); - 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 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 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" - ); - } - - // - // Test with invalid signature - // - { - // Prepare the claims object - let claims = claims.clone(); - // Create the token - let key = EncodingKey::from_secret("invalid".as_ref()); - 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 token: {:?}", res); - } - - // - // Test with valid token invalid access method - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.ac = Some("invalid".to_string()); - // 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 token: {:?}", res); - } - - // - // Test with invalid id - // - { - // Prepare the claims object - let mut claims = claims.clone(); - claims.id = Some("##_INVALID_##".to_string()); - // 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 token: {:?}", res); - } - - // - // Test with generic user identifier - // - { - let resource_id = "user:2k9qnabxuxh8k4d5gfto".to_string(); - // Prepare the claims object - let mut claims = claims.clone(); - claims.id = Some(resource_id.clone()); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), resource_id); - assert!(sess.au.is_record()); - let user_id = syn::thing(&resource_id).unwrap(); - assert_eq!(sess.rd, Some(Value::from(user_id))); - } - - // - // Test with custom user numeric identifiers of varying sizes - // - { - let ids = ["1", "2", "100", "10000000"]; - for id in ids.iter() { - let resource_id = format!("user:{id}"); + for id in &case.ids { // Prepare the claims object let mut claims = claims.clone(); - claims.id = Some(resource_id.clone()); + claims.id = Some(id.to_string()); + claims.roles = + case.roles.clone().map(|roles| roles.into_iter().map(String::from).collect()); + // Create the token + let key = EncodingKey::from_secret(case.key.as_ref()); let enc = encode(&HEADER, &claims, &key).unwrap(); - // Signin with the token + + // Authenticate with the token let mut sess = Session::default(); let res = token(&ds, &mut sess, &enc).await; - assert!(res.is_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), resource_id); - assert!(sess.au.is_record()); - let user_id = syn::thing(&resource_id).unwrap(); - assert_eq!(sess.rd, Some(Value::from(user_id))); + if case.expect_error { + assert!(res.is_err(), "Unexpected success for case: {:?}", case); + } else { + assert!(res.is_ok(), "Failed to sign in with token for case: {:?}", case); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, Some("test".to_string())); + assert_eq!(sess.au.id(), *id); + + // Ensure record users do not have roles + for role in &AVAILABLE_ROLES { + assert!( + !sess.au.has_role(role), + "Auth user expected to not have role {:?} in case: {:?}", + role, + case + ); + } + + // Ensure that the expiration is set correctly + let exp = sess.exp.unwrap(); + 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 in case: {:?}", + case + ); + } } } - - // - // Test with custom user string identifiers of varying lengths - // - { - let ids = ["username", "username1", "username10", "username100"]; - for id in ids.iter() { - let resource_id = format!("user:{id}"); - // Prepare the claims object - let mut claims = claims.clone(); - claims.id = Some(resource_id.clone()); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), resource_id); - assert!(sess.au.is_record()); - let user_id = syn::thing(&resource_id).unwrap(); - assert_eq!(sess.rd, Some(Value::from(user_id))); - } - } - - // - // Test with custom user string identifiers of varying lengths with special characters - // - { - let ids = ["user.name", "user.name1", "user.name10", "user.name100"]; - for id in ids.iter() { - // Enclose special characters in "⟨brackets⟩" - let resource_id = format!("user:⟨{id}⟩"); - // Prepare the claims object - let mut claims = claims.clone(); - claims.id = Some(resource_id.clone()); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), resource_id); - assert!(sess.au.is_record()); - let user_id = syn::thing(&resource_id).unwrap(); - assert_eq!(sess.rd, Some(Value::from(user_id))); - } - } - - // - // Test with custom UUID user identifier - // - { - let id = "83149446-95f5-4c0d-9f42-136e7b272456"; - // Enclose special characters in "⟨brackets⟩" - let resource_id = format!("user:⟨{id}⟩"); - // Prepare the claims object - let mut claims = claims.clone(); - claims.id = Some(resource_id.clone()); - // 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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("token".to_string())); - assert_eq!(sess.au.id(), resource_id); - assert!(sess.au.is_record()); - let user_id = syn::thing(&resource_id).unwrap(); - assert_eq!(sess.rd, Some(Value::from(user_id))); - } } #[tokio::test] - async fn test_token_db_record_custom_claims() { + async fn test_token_record_custom_claims() { use std::collections::HashMap; let secret = "jwt_secret"; @@ -1715,7 +1260,7 @@ mod tests { #[cfg(feature = "jwks")] #[tokio::test] - async fn test_token_db_record_jwks() { + async fn test_token_record_jwks() { use crate::dbs::capabilities::{Capabilities, NetTarget, Targets}; use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; use jsonwebtoken::jwk::{Jwk, JwkSet}; @@ -1971,7 +1516,193 @@ mod tests { } #[tokio::test] - async fn test_token_db_record_and_authenticate_clause() { + async fn test_token_authenticate_clause() { + #[derive(Debug)] + struct TestCase { + title: &'static str, + iss_claim: Option<&'static str>, + aud_claim: Option, + error_statement: &'static str, + expected_error: Option, + } + + let test_cases = vec![ + TestCase { + title: "with correct 'iss' and 'aud' claims", + iss_claim: Some("surrealdb-test"), + aud_claim: Some(Audience::Single("surrealdb-test".to_string())), + error_statement: "THROW", + expected_error: None, + }, + TestCase { + title: "with correct 'iss' and 'aud' claims, multiple audiences", + iss_claim: Some("surrealdb-test"), + aud_claim: Some(Audience::Multiple(vec![ + "invalid".to_string(), + "surrealdb-test".to_string(), + ])), + error_statement: "THROW", + expected_error: None, + }, + TestCase { + title: "with correct 'iss' claim but invalid 'aud' claim", + iss_claim: Some("surrealdb-test"), + aud_claim: Some(Audience::Single("invalid".to_string())), + error_statement: "THROW", + expected_error: Some(Error::Thrown("Invalid token audience string".to_string())), + }, + TestCase { + title: "with correct 'iss' claim but invalid 'aud' claim, multiple audiences", + iss_claim: Some("surrealdb-test"), + aud_claim: Some(Audience::Multiple(vec![ + "invalid".to_string(), + "surrealdb-test-different".to_string(), + ])), + error_statement: "THROW", + expected_error: Some(Error::Thrown("Invalid token audience array".to_string())), + }, + TestCase { + title: "with correct 'iss' claim but invalid 'aud' claim, generic error", + iss_claim: Some("surrealdb-test"), + aud_claim: Some(Audience::Single("invalid".to_string())), + error_statement: "RETURN", + expected_error: Some(Error::InvalidAuth), + }, + ]; + + let test_levels = vec![ + TestLevel { + level: "ROOT", + ns: None, + db: None, + }, + TestLevel { + level: "NS", + ns: Some("test"), + db: None, + }, + TestLevel { + level: "DB", + ns: Some("test"), + db: Some("test"), + }, + ]; + + let secret = "secret"; + let key = EncodingKey::from_secret(secret.as_ref()); + let claims = Claims { + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ac: Some("user".to_string()), + ..Claims::default() + }; + + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + + for level in &test_levels { + for case in &test_cases { + println!("Test case: {} level {}", level.level, case.title); + + ds.execute( + format!( + r#" + REMOVE ACCESS IF EXISTS user ON {0}; + DEFINE ACCESS user ON {0} TYPE JWT + ALGORITHM HS512 KEY '{1}' + AUTHENTICATE {{ + IF $token.iss != "surrealdb-test" {{ {2} "Invalid token issuer" }}; + IF type::is::array($token.aud) {{ + IF "surrealdb-test" NOT IN $token.aud {{ {2} "Invalid token audience array" }} + }} ELSE {{ + IF $token.aud IS NOT "surrealdb-test" {{ {2} "Invalid token audience string" }} + }}; + }} + DURATION FOR SESSION 2h + ; + "#, + level.level, secret, case.error_statement, + ) + .as_str(), + &sess, + None, + ) + .await + .unwrap(); + + // Prepare the claims object + let mut claims = claims.clone(); + claims.ns = level.ns.map(|s| s.to_string()); + claims.db = level.db.map(|s| s.to_string()); + claims.iss = case.iss_claim.map(|s| s.to_string()); + claims.aud = case.aud_claim.clone(); + + // 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; + + if let Some(expected_err) = &case.expected_error { + assert!(res.is_err(), "Unexpected success for case: {:?}", case); + let err = res.unwrap_err(); + match (expected_err, &err) { + (Error::InvalidAuth, Error::InvalidAuth) => {} + (Error::Thrown(expected_msg), Error::Thrown(msg)) + if expected_msg == msg => {} + _ => panic!("Unexpected error for case: {:?}, got: {:?}", case, err), + } + } else { + assert!(res.is_ok(), "Failed to sign in with token for case: {:?}", case); + assert_eq!(sess.ns, level.ns.map(|s| s.to_string())); + assert_eq!(sess.db, level.db.map(|s| s.to_string())); + assert_eq!(sess.ac, Some("user".to_string())); + assert_eq!(sess.au.id(), "user"); + + // Check auth level + assert_eq!(sess.au.level().ns(), level.ns); + assert_eq!(sess.au.level().db(), level.db); + match level.level { + "ROOT" => assert!(sess.au.is_root()), + "NS" => assert!(sess.au.is_ns()), + "DB" => assert!(sess.au.is_db()), + _ => panic!("Unsupported level"), + } + + // Check roles + 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" + ); + + // Check expiration + let exp = sess.exp.unwrap(); + 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 match the defined duration in case: {:?}", + case + ); + } + } + } + } + + #[tokio::test] + async fn test_token_record_and_authenticate_clause() { // Test with an "id" claim { let secret = "jwt_secret"; @@ -2241,992 +1972,4 @@ mod tests { } } } - - #[tokio::test] - async fn test_token_db_authenticate_clause() { - // Test with correct "iss" and "aud" claims - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("surrealdb-test".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - db: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON DATABASE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_db()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), Some("test")); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" and "aud" claims, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "invalid".to_string(), - "surrealdb-test".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - db: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON DATABASE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_db()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), Some("test")); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" claim but incorrect "aud" claim - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - db: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON DATABASE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "surrealdb-test-different".to_string(), - "invalid".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - db: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON DATABASE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim - // In this case, something is returned by the clause, which returns a generic error - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - db: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON DATABASE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - } - - #[tokio::test] - async fn test_token_ns_authenticate_clause() { - // Test with correct "iss" and "aud" claims - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("surrealdb-test".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON NAMESPACE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" and "aud" claims, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "invalid".to_string(), - "surrealdb-test".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON NAMESPACE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, Some("test".to_string())); - assert_eq!(sess.db, None); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_ns()); - assert_eq!(sess.au.level().ns(), Some("test")); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" claim but incorrect "aud" claim - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON NAMESPACE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "surrealdb-test-different".to_string(), - "invalid".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON NAMESPACE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim - // In this case, something is returned by the clause, which returns a generic error - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ns: Some("test".to_string()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON NAMESPACE TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - } - - #[tokio::test] - async fn test_token_root_authenticate_clause() { - // Test with correct "iss" and "aud" claims - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("surrealdb-test".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON ROOT TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" and "aud" claims, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "invalid".to_string(), - "surrealdb-test".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON ROOT TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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_ok(), "Failed to signin with token: {:?}", res); - assert_eq!(sess.ns, None); - assert_eq!(sess.db, None); - assert_eq!(sess.ac, Some("user".to_string())); - assert!(sess.au.is_root()); - assert_eq!(sess.au.level().ns(), None); - assert_eq!(sess.au.level().db(), None); - // Record users should not have roles - 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"); - // 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(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 the defined duration" - ); - } - - // Test with correct "iss" claim but incorrect "aud" claim - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON ROOT TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience string" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim, with multiple audiences - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Multiple(vec![ - "surrealdb-test-different".to_string(), - "invalid".to_string(), - ])), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner(); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON ROOT TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ THROW "Invalid token audience array" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ THROW "Invalid token audience string" }} - }}; - }} - DURATION FOR SESSION 2h - "# - ) - .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; - - match res { - Err(Error::Thrown(e)) if e == "Invalid token audience array" => {} // ok - res => panic!( - "Expected authentication to failed due to invalid token audience array, but instead received: {:?}", - res - ), - } - } - - // Test with correct "iss" claim but incorrect "aud" claim - // In this case, something is returned by the clause, which returns a generic error - { - let secret = "jwt_secret"; - let key = EncodingKey::from_secret(secret.as_ref()); - let claims = Claims { - iss: Some("surrealdb-test".to_string()), - aud: Some(Audience::Single("invalid".to_string())), - iat: Some(Utc::now().timestamp()), - nbf: Some(Utc::now().timestamp()), - exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - ac: Some("user".to_string()), - ..Claims::default() - }; - - let ds = Datastore::new("memory").await.unwrap(); - let sess = Session::owner().with_ns("test").with_db("test"); - ds.execute( - format!( - r#" - DEFINE ACCESS user ON ROOT TYPE JWT - ALGORITHM HS512 KEY '{secret}' - AUTHENTICATE {{ - IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }}; - IF type::is::array($token.aud) {{ - IF "surrealdb-test" NOT IN $token.aud {{ RETURN "FAIL" }} - }} ELSE {{ - IF $token.aud IS NOT "surrealdb-test" {{ RETURN "FAIL" }} - }}; - }} - DURATION FOR SESSION 2h - ; - "# - ) - .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; - - match res { - Err(Error::InvalidAuth) => {} // ok - res => panic!( - "Expected a generic authentication error, but instead received: {:?}", - res - ), - } - } - } }