From e281a4e41eb54e38c2be96eac85a083a2ca4d980 Mon Sep 17 00:00:00 2001 From: Gerard Guillemas Martos Date: Mon, 15 Jul 2024 12:59:33 +0200 Subject: [PATCH] Allow defining JWT access at the root level (#4348) --- core/src/err/mod.rs | 26 ++-- core/src/iam/verify.rs | 155 +++++++++++++++++++++++ core/src/key/mod.rs | 1 + core/src/kvs/cache.rs | 1 + core/src/kvs/tx.rs | 44 ++++++- core/src/sql/statements/define/access.rs | 31 +++++ core/src/sql/statements/info.rs | 6 + core/src/sql/statements/remove/access.rs | 20 ++- core/src/syn/parser/stmt/define.rs | 8 ++ core/src/syn/parser/test/stmt.rs | 82 ++++++++++++ lib/tests/define.rs | 55 +++++++- lib/tests/info.rs | 31 +++-- lib/tests/remove.rs | 52 +++++++- lib/tests/strict.rs | 1 + 14 files changed, 469 insertions(+), 44 deletions(-) diff --git a/core/src/err/mod.rs b/core/src/err/mod.rs index 604f2821..c236543b 100644 --- a/core/src/err/mod.rs +++ b/core/src/err/mod.rs @@ -298,12 +298,6 @@ pub enum Error { value: String, }, - /// The requested namespace access method does not exist - #[error("The namespace access method '{value}' does not exist")] - NaNotFound { - value: String, - }, - /// The requested namespace login does not exist #[error("The namespace login '{value}' does not exist")] NlNotFound { @@ -316,12 +310,6 @@ pub enum Error { value: String, }, - /// The requested database access method does not exist - #[error("The database access method '{value}' does not exist")] - DaNotFound { - value: String, - }, - /// The requested database login does not exist #[error("The database login '{value}' does not exist")] DlNotFound { @@ -948,15 +936,18 @@ pub enum Error { }, /// The requested namespace access method already exists - #[error("The namespace access method '{value}' already exists")] + #[error("The access method '{value}' already exists in the namespace '{ns}'")] AccessNsAlreadyExists { value: String, + ns: String, }, /// The requested database access method already exists - #[error("The database access method '{value}' already exists")] + #[error("The access method '{value}' already exists in the database '{db}'")] AccessDbAlreadyExists { value: String, + ns: String, + db: String, }, /// The requested root access method does not exist @@ -966,15 +957,18 @@ pub enum Error { }, /// The requested namespace access method does not exist - #[error("The namespace access method '{value}' does not exist")] + #[error("The access method '{value}' does not exist in the namespace '{ns}'")] AccessNsNotFound { value: String, + ns: String, }, /// The requested database access method does not exist - #[error("The database access method '{value}' does not exist")] + #[error("The access method '{value}' does not exist in the database '{db}'")] AccessDbNotFound { value: String, + ns: String, + db: String, }, /// The access method cannot be defined on the requested level diff --git a/core/src/iam/verify.rs b/core/src/iam/verify.rs index 57d5f4f4..be6d0538 100644 --- a/core/src/iam/verify.rs +++ b/core/src/iam/verify.rs @@ -474,6 +474,57 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul ))); Ok(()) } + // Check if this is root access + Claims { + ac: Some(ac), + .. + } => { + // Log the decoded authentication claims + trace!("Authenticating to root with access method `{}`", ac); + // Create a new readonly transaction + let mut tx = kvs.transaction(Read, Optimistic).await?; + // Get the namespace access method + let de = tx.get_root_access(&ac).await?; + // Obtain the configuration to verify the token based on the access method + let cf = match de.kind { + AccessType::Jwt(ac) => match ac.verify { + JwtAccessVerify::Key(key) => config(key.alg, key.key), + #[cfg(feature = "jwks")] + JwtAccessVerify::Jwks(jwks) => { + if let Some(kid) = token_data.header.kid { + jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await + } else { + Err(Error::MissingTokenHeader("kid".to_string())) + } + } + #[cfg(not(feature = "jwks"))] + _ => return Err(Error::AccessMethodMismatch), + }, + _ => return Err(Error::AccessMethodMismatch), + }?; + // Verify the token + decode::(token, &cf.0, &cf.1)?; + // Parse the roles + let roles = match token_data.claims.roles { + // If no role is provided, grant the viewer role + None => vec![Role::Viewer], + // If roles are provided, parse them + Some(roles) => roles + .iter() + .map(|r| -> Result { + Role::from_str(r.as_str()).map_err(Error::IamError) + }) + .collect::, _>>()?, + }; + // Log the success + trace!("Authenticated to root with access method `{}`", ac); + // Set the session + session.tk = Some(value); + session.ac = Some(ac.to_owned()); + session.exp = expiration(de.duration.session)?; + session.au = Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Root))); + Ok(()) + } // Check if this is root authentication with user credentials Claims { id: Some(id), @@ -838,6 +889,110 @@ mod tests { } } + #[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()), + nbf: Some(Utc::now().timestamp()), + exp: Some((Utc::now() + Duration::hours(1)).timestamp()), + ac: Some("token".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 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"; diff --git a/core/src/key/mod.rs b/core/src/key/mod.rs index 44285d82..e5529362 100644 --- a/core/src/key/mod.rs +++ b/core/src/key/mod.rs @@ -1,6 +1,7 @@ //! How the keys are structured in the key value store /// /// crate::key::root::all / +/// crate::key::root::ac /!ac{ac} /// crate::key::root::hb /!hb{ts}/{nd} /// crate::key::root::nd /!nd{nd} /// crate::key::root::ni /!ni diff --git a/core/src/kvs/cache.rs b/core/src/kvs/cache.rs index 8fea22fb..6819d0a7 100644 --- a/core/src/kvs/cache.rs +++ b/core/src/kvs/cache.rs @@ -28,6 +28,7 @@ pub enum Entry { Pa(Arc), Tb(Arc), // Multi definitions + Acs(Arc<[DefineAccessStatement]>), Azs(Arc<[DefineAnalyzerStatement]>), Dbs(Arc<[DefineDatabaseStatement]>), Das(Arc<[DefineAccessStatement]>), diff --git a/core/src/kvs/tx.rs b/core/src/kvs/tx.rs index 683bdee2..3adf6e62 100644 --- a/core/src/kvs/tx.rs +++ b/core/src/kvs/tx.rs @@ -1328,6 +1328,34 @@ impl Transaction { Ok(val) } + /// Retrieve all ROOT access method definitions. + pub async fn all_root_accesses(&mut self) -> Result, Error> { + let key = crate::key::root::ac::prefix(); + Ok(if let Some(e) = self.cache.get(&key) { + if let Entry::Acs(v) = e { + v + } else { + unreachable!(); + } + } else { + let beg = crate::key::root::ac::prefix(); + let end = crate::key::root::ac::suffix(); + let val = self.getr(beg..end, u32::MAX).await?; + let val = val.convert().into(); + self.cache.set(key, Entry::Acs(Arc::clone(&val))); + val + }) + } + + /// Retrieve all ROOT access method definitions in redacted form. + pub async fn all_root_accesses_redacted( + &mut self, + ) -> Result, Error> { + let accesses = self.all_root_accesses().await?; + let redacted: Vec<_> = accesses.iter().map(|statement| statement.redacted()).collect(); + Ok(Arc::from(redacted)) + } + /// Retrieve all namespace definitions in a datastore. pub async fn all_ns(&mut self) -> Result, Error> { let key = crate::key::root::ns::prefix(); @@ -1741,6 +1769,15 @@ impl Transaction { Ok(val.into()) } + /// Retrieve a specific root access method definition. + pub async fn get_root_access(&mut self, ac: &str) -> Result { + let key = crate::key::root::ac::new(ac); + let val = self.get(key).await?.ok_or(Error::AccessRootNotFound { + value: ac.to_owned(), + })?; + Ok(val.into()) + } + /// Retrieve a specific namespace definition. pub async fn get_ns(&mut self, ns: &str) -> Result { let key = crate::key::root::ns::new(ns); @@ -1771,8 +1808,9 @@ impl Transaction { ac: &str, ) -> Result { let key = crate::key::namespace::ac::new(ns, ac); - let val = self.get(key).await?.ok_or(Error::NaNotFound { + let val = self.get(key).await?.ok_or(Error::AccessNsNotFound { value: ac.to_owned(), + ns: ns.to_owned(), })?; Ok(val.into()) } @@ -1825,8 +1863,10 @@ impl Transaction { ac: &str, ) -> Result { let key = crate::key::database::ac::new(ns, db, ac); - let val = self.get(key).await?.ok_or(Error::DaNotFound { + let val = self.get(key).await?.ok_or(Error::AccessDbNotFound { value: ac.to_owned(), + ns: ns.to_owned(), + db: db.to_owned(), })?; Ok(val.into()) } diff --git a/core/src/sql/statements/define/access.rs b/core/src/sql/statements/define/access.rs index df29b08c..bf4370f1 100644 --- a/core/src/sql/statements/define/access.rs +++ b/core/src/sql/statements/define/access.rs @@ -60,6 +60,34 @@ impl DefineAccessStatement { opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?; match &self.base { + Base::Root => { + // Claim transaction + let mut run = ctx.tx_lock().await; + // Clear the cache + run.clear_cache(); + // Check if access method already exists + if run.get_root_access(&self.name).await.is_ok() { + if self.if_not_exists { + return Ok(Value::None); + } else { + return Err(Error::AccessRootAlreadyExists { + value: self.name.to_string(), + }); + } + } + // Process the statement + let key = crate::key::root::ac::new(&self.name); + run.set( + key, + DefineAccessStatement { + if_not_exists: false, + ..self.clone() + }, + ) + .await?; + // Ok all good + Ok(Value::None) + } Base::Ns => { // Claim transaction let mut run = ctx.tx_lock().await; @@ -72,6 +100,7 @@ impl DefineAccessStatement { } else { return Err(Error::AccessNsAlreadyExists { value: self.name.to_string(), + ns: opt.ns()?.into(), }); } } @@ -101,6 +130,8 @@ impl DefineAccessStatement { } else { return Err(Error::AccessDbAlreadyExists { value: self.name.to_string(), + ns: opt.ns()?.into(), + db: opt.db()?.into(), }); } } diff --git a/core/src/sql/statements/info.rs b/core/src/sql/statements/info.rs index 591c6581..ed7e38f6 100644 --- a/core/src/sql/statements/info.rs +++ b/core/src/sql/statements/info.rs @@ -91,6 +91,12 @@ impl InfoStatement { tmp.insert(v.name.to_string(), v.to_string().into()); } res.insert("users".to_owned(), tmp.into()); + // Process the accesses + let mut tmp = Object::default(); + for v in run.all_root_accesses_redacted().await?.iter() { + tmp.insert(v.name.to_string(), v.to_string().into()); + } + res.insert("accesses".to_owned(), tmp.into()); // Ok all good Value::from(res).ok() } diff --git a/core/src/sql/statements/remove/access.rs b/core/src/sql/statements/remove/access.rs index 318f9cdc..fcf4f170 100644 --- a/core/src/sql/statements/remove/access.rs +++ b/core/src/sql/statements/remove/access.rs @@ -27,6 +27,19 @@ impl RemoveAccessStatement { opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?; match &self.base { + Base::Root => { + // Claim transaction + let mut run = ctx.tx_lock().await; + // Clear the cache + run.clear_cache(); + // Get the definition + let ac = run.get_root_access(&self.name).await?; + // Delete the definition + let key = crate::key::root::ac::new(&ac.name); + run.del(key).await?; + // Ok all good + Ok(Value::None) + } Base::Ns => { // Claim transaction let mut run = ctx.tx_lock().await; @@ -59,10 +72,13 @@ impl RemoveAccessStatement { .await; match future { Err(e) if self.if_exists => match e { - Error::NaNotFound { + Error::AccessRootNotFound { .. } => Ok(Value::None), - Error::DaNotFound { + Error::AccessNsNotFound { + .. + } => Ok(Value::None), + Error::AccessDbNotFound { .. } => Ok(Value::None), e => Err(e), diff --git a/core/src/syn/parser/stmt/define.rs b/core/src/syn/parser/stmt/define.rs index cf3a47e0..5bb809ed 100644 --- a/core/src/syn/parser/stmt/define.rs +++ b/core/src/syn/parser/stmt/define.rs @@ -287,6 +287,14 @@ impl Parser<'_> { } t!("RECORD") => { self.pop_peek(); + // The record access type can only be defined at the database level + if !matches!(res.base, Base::Db) { + unexpected!( + self, + t!("RECORD"), + "a valid access type at this level" + ); + } let mut ac = access_type::RecordAccess { ..Default::default() }; diff --git a/core/src/syn/parser/test/stmt.rs b/core/src/syn/parser/test/stmt.rs index 3d084b23..c51ce6d2 100644 --- a/core/src/syn/parser/test/stmt.rs +++ b/core/src/syn/parser/test/stmt.rs @@ -756,6 +756,66 @@ fn parse_define_access_jwt_key() { res ); } + // With comment. Asymmetric verify only. On namespace level. + { + let res = test_parse!( + parse_stmt, + r#"DEFINE ACCESS a ON NAMESPACE TYPE JWT ALGORITHM EDDSA KEY "foo" COMMENT "bar""# + ) + .unwrap(); + assert_eq!( + res, + Statement::Define(DefineStatement::Access(DefineAccessStatement { + name: Ident("a".to_string()), + base: Base::Ns, + kind: AccessType::Jwt(JwtAccess { + verify: JwtAccessVerify::Key(JwtAccessVerifyKey { + alg: Algorithm::EdDSA, + key: "foo".to_string(), + }), + issue: None, + }), + // Default durations. + duration: AccessDuration { + grant: None, + token: Some(Duration::from_hours(1)), + session: None, + }, + comment: Some(Strand("bar".to_string())), + if_not_exists: false, + })), + ) + } + // With comment. Asymmetric verify only. On root level. + { + let res = test_parse!( + parse_stmt, + r#"DEFINE ACCESS a ON ROOT TYPE JWT ALGORITHM EDDSA KEY "foo" COMMENT "bar""# + ) + .unwrap(); + assert_eq!( + res, + Statement::Define(DefineStatement::Access(DefineAccessStatement { + name: Ident("a".to_string()), + base: Base::Root, + kind: AccessType::Jwt(JwtAccess { + verify: JwtAccessVerify::Key(JwtAccessVerifyKey { + alg: Algorithm::EdDSA, + key: "foo".to_string(), + }), + issue: None, + }), + // Default durations. + duration: AccessDuration { + grant: None, + token: Some(Duration::from_hours(1)), + session: None, + }, + comment: Some(Strand("bar".to_string())), + if_not_exists: false, + })), + ) + } } #[test] @@ -1132,6 +1192,28 @@ fn parse_define_access_record() { res ); } + // Attempt to define record access at the root level. + { + let res = test_parse!( + parse_stmt, + r#"DEFINE ACCESS a ON ROOT TYPE RECORD DURATION FOR TOKEN NONE"# + ); + assert!( + res.is_err(), + "Unexpected successful parsing of record access at root level: {:?}", + res + ); + } + // Attempt to define record access at the namespace level. + { + let res = + test_parse!(parse_stmt, r#"DEFINE ACCESS a ON NS TYPE RECORD DURATION FOR TOKEN NONE"#); + assert!( + res.is_err(), + "Unexpected successful parsing of record access at namespace level: {:?}", + res + ); + } } #[test] diff --git a/lib/tests/define.rs b/lib/tests/define.rs index c593694d..3dc3811f 100644 --- a/lib/tests/define.rs +++ b/lib/tests/define.rs @@ -29,6 +29,7 @@ async fn define_statement_namespace() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, namespaces: { test: 'DEFINE NAMESPACE test' }, users: {}, }", @@ -1273,8 +1274,8 @@ async fn permissions_checks_define_ns() { // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], - vec!["{ namespaces: { }, users: { } }"], + vec!["{ accesses: { }, namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"], ]; let test_cases = [ @@ -1428,6 +1429,48 @@ async fn permissions_checks_define_analyzer() { assert!(res.is_ok(), "{}", res.unwrap_err()); } +#[tokio::test] +async fn permissions_checks_define_access_root() { + let scenario = HashMap::from([ + ("prepare", ""), + ("test", "DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY 'secret'"), + ("check", "INFO FOR ROOT"), + ]); + + // Define the expected results for the check statement when the test statement succeeded and when it failed + let check_results = [ + vec!["{ accesses: { access: \"DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, namespaces: { }, users: { } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"] + ]; + + let test_cases = [ + // Root level + ((().into(), Role::Owner), ("NS", "DB"), true), + ((().into(), Role::Editor), ("NS", "DB"), false), + ((().into(), Role::Viewer), ("NS", "DB"), false), + // Namespace level + ((("NS",).into(), Role::Owner), ("NS", "DB"), false), + ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false), + ((("NS",).into(), Role::Editor), ("NS", "DB"), false), + ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false), + ((("NS",).into(), Role::Viewer), ("NS", "DB"), false), + ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false), + // Database level + ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false), + ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false), + ]; + + let res = iam_check_cases(test_cases.iter(), &scenario, check_results).await; + assert!(res.is_ok(), "{}", res.unwrap_err()); +} + #[tokio::test] async fn permissions_checks_define_access_ns() { let scenario = HashMap::from([ @@ -1522,8 +1565,8 @@ async fn permissions_checks_define_user_root() { // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"], - vec!["{ namespaces: { }, users: { } }"] + vec!["{ accesses: { }, namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"] ]; let test_cases = [ @@ -2105,9 +2148,9 @@ async fn define_remove_access() -> Result<(), Error> { let mut t = Test::new(sql).await?; t.skip_ok(1)?; t.expect_val("None")?; - t.expect_error("The database access method 'example' already exists")?; + t.expect_error("The access method 'example' already exists in the database 'test'")?; t.skip_ok(1)?; - t.expect_error("The database access method 'example' does not exist")?; + t.expect_error("The access method 'example' does not exist in the database 'test'")?; t.expect_val("None")?; Ok(()) } diff --git a/lib/tests/info.rs b/lib/tests/info.rs index b4d8e2e5..2e80226f 100644 --- a/lib/tests/info.rs +++ b/lib/tests/info.rs @@ -12,19 +12,22 @@ async fn info_for_root() { let sql = r#" DEFINE NAMESPACE NS; DEFINE USER user ON ROOT PASSWORD 'pass'; + DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY 'secret'; INFO FOR ROOT "#; let dbs = new_ds().await.unwrap(); let ses = Session::owner(); let mut res = dbs.execute(sql, &ses, None).await.unwrap(); - assert_eq!(res.len(), 3); + assert_eq!(res.len(), 4); let out = res.pop().unwrap().output(); assert!(out.is_ok(), "Unexpected error: {:?}", out); - let output_regex = - Regex::new(r"\{ namespaces: \{ NS: .* \}, users: \{ user: .* \} \}").unwrap(); + let output_regex = Regex::new( + r"\{ accesses: \{ access: .* \}, namespaces: \{ NS: .* \}, users: \{ user: .* \} \}", + ) + .unwrap(); let out_str = out.unwrap().to_string(); assert!( output_regex.is_match(&out_str), @@ -209,8 +212,10 @@ async fn permissions_checks_info_root() { HashMap::from([("prepare", ""), ("test", "INFO FOR ROOT"), ("check", "INFO FOR ROOT")]); // Define the expected results for the check statement when the test statement succeeded and when it failed - let check_results = - [vec!["{ namespaces: { }, users: { } }"], vec!["{ namespaces: { }, users: { } }"]]; + let check_results = [ + vec!["{ accesses: { }, namespaces: { }, users: { } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"], + ]; let test_cases = [ // Root level @@ -537,11 +542,11 @@ async fn access_info_redacted() { // Record { let sql = r#" - DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret'; - INFO FOR NS + DEFINE ACCESS access ON DB TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret'; + INFO FOR DB "#; let dbs = new_ds().await.unwrap(); - let ses = Session::owner().with_ns("ns"); + let ses = Session::owner().with_ns("ns").with_db("test"); let mut res = dbs.execute(sql, &ses, None).await.unwrap(); assert_eq!(res.len(), 2); @@ -550,7 +555,7 @@ async fn access_info_redacted() { assert!(out.is_ok(), "Unexpected error: {:?}", out); let out_expected = - r#"{ accesses: { access: "DEFINE ACCESS access ON NAMESPACE TYPE RECORD WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE" }, databases: { }, users: { } }"#.to_string(); + r#"{ accesses: { access: "DEFINE ACCESS access ON DATABASE TYPE RECORD WITH JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"#.to_string(); let out_str = out.unwrap().to_string(); assert_eq!( out_str, out_expected, @@ -610,11 +615,11 @@ async fn access_info_redacted_structure() { // Record { let sql = r#" - DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h; - INFO FOR NS STRUCTURE + DEFINE ACCESS access ON DB TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h; + INFO FOR DB STRUCTURE "#; let dbs = new_ds().await.unwrap(); - let ses = Session::owner().with_ns("ns"); + let ses = Session::owner().with_ns("ns").with_db("db"); let mut res = dbs.execute(sql, &ses, None).await.unwrap(); assert_eq!(res.len(), 2); @@ -623,7 +628,7 @@ async fn access_info_redacted_structure() { assert!(out.is_ok(), "Unexpected error: {:?}", out); let out_expected = - r#"{ accesses: [{ base: 'NAMESPACE', duration: { session: 6h, token: 15m }, kind: { jwt: { issuer: { alg: 'HS512', key: '[REDACTED]' }, verify: { alg: 'HS512', key: '[REDACTED]' } }, kind: 'RECORD' }, name: 'access' }], databases: [], users: [] }"#.to_string(); + r#"{ accesses: [{ base: 'DATABASE', duration: { session: 6h, token: 15m }, kind: { jwt: { issuer: { alg: 'HS512', key: '[REDACTED]' }, verify: { alg: 'HS512', key: '[REDACTED]' } }, kind: 'RECORD' }, name: 'access' }], analyzers: [], functions: [], models: [], params: [], tables: [], users: [] }"#.to_string(); let out_str = out.unwrap().to_string(); assert_eq!( out_str, out_expected, diff --git a/lib/tests/remove.rs b/lib/tests/remove.rs index 9b525e0a..6b670923 100644 --- a/lib/tests/remove.rs +++ b/lib/tests/remove.rs @@ -522,7 +522,7 @@ async fn should_error_when_remove_and_access_does_not_exist() -> Result<(), Erro assert_eq!(res.len(), 1); // let tmp = res.remove(0).result.unwrap_err(); - assert!(matches!(tmp, Error::DaNotFound { .. }),); + assert!(matches!(tmp, Error::AccessDbNotFound { .. }),); Ok(()) } @@ -589,8 +589,8 @@ async fn permissions_checks_remove_ns() { // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ namespaces: { }, users: { } }"], - vec!["{ namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"], + vec!["{ accesses: { }, namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], ]; let test_cases = [ @@ -747,6 +747,48 @@ async fn permissions_checks_remove_analyzer() { assert!(res.is_ok(), "{}", res.unwrap_err()); } +#[tokio::test] +async fn permissions_checks_remove_root_access() { + let scenario = HashMap::from([ + ("prepare", "DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY 'secret'"), + ("test", "REMOVE ACCESS access ON ROOT"), + ("check", "INFO FOR ROOT"), + ]); + + // Define the expected results for the check statement when the test statement succeeded and when it failed + let check_results = [ + vec!["{ accesses: { }, namespaces: { }, users: { } }"], + vec!["{ accesses: { access: \"DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY '[REDACTED]' WITH ISSUER KEY '[REDACTED]' DURATION FOR TOKEN 1h, FOR SESSION NONE\" }, namespaces: { }, users: { } }"], + ]; + + let test_cases = [ + // Root level + ((().into(), Role::Owner), ("NS", "DB"), true), + ((().into(), Role::Editor), ("NS", "DB"), false), + ((().into(), Role::Viewer), ("NS", "DB"), false), + // Namespace level + ((("NS",).into(), Role::Owner), ("NS", "DB"), false), + ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false), + ((("NS",).into(), Role::Editor), ("NS", "DB"), false), + ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false), + ((("NS",).into(), Role::Viewer), ("NS", "DB"), false), + ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false), + // Database level + ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false), + ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false), + ((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false), + ]; + + let res = iam_check_cases(test_cases.iter(), &scenario, check_results).await; + assert!(res.is_ok(), "{}", res.unwrap_err()); +} + #[tokio::test] async fn permissions_checks_remove_ns_access() { let scenario = HashMap::from([ @@ -841,8 +883,8 @@ async fn permissions_checks_remove_root_user() { // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ namespaces: { }, users: { } }"], - vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"], + vec!["{ accesses: { }, namespaces: { }, users: { } }"], + vec!["{ accesses: { }, namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"], ]; let test_cases = [ diff --git a/lib/tests/strict.rs b/lib/tests/strict.rs index 2b52130e..cc4fbf1c 100644 --- a/lib/tests/strict.rs +++ b/lib/tests/strict.rs @@ -233,6 +233,7 @@ async fn loose_mode_all_ok() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, namespaces: { test: 'DEFINE NAMESPACE test' }, users: {}, }",