Allow defining JWT access at the root level (#4348)

This commit is contained in:
Gerard Guillemas Martos 2024-07-15 12:59:33 +02:00 committed by GitHub
parent fc154142fa
commit e281a4e41e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 469 additions and 44 deletions

View file

@ -298,12 +298,6 @@ pub enum Error {
value: String, 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 /// The requested namespace login does not exist
#[error("The namespace login '{value}' does not exist")] #[error("The namespace login '{value}' does not exist")]
NlNotFound { NlNotFound {
@ -316,12 +310,6 @@ pub enum Error {
value: String, 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 /// The requested database login does not exist
#[error("The database login '{value}' does not exist")] #[error("The database login '{value}' does not exist")]
DlNotFound { DlNotFound {
@ -948,15 +936,18 @@ pub enum Error {
}, },
/// The requested namespace access method already exists /// 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 { AccessNsAlreadyExists {
value: String, value: String,
ns: String,
}, },
/// The requested database access method already exists /// 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 { AccessDbAlreadyExists {
value: String, value: String,
ns: String,
db: String,
}, },
/// The requested root access method does not exist /// The requested root access method does not exist
@ -966,15 +957,18 @@ pub enum Error {
}, },
/// The requested namespace access method does not exist /// 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 { AccessNsNotFound {
value: String, value: String,
ns: String,
}, },
/// The requested database access method does not exist /// 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 { AccessDbNotFound {
value: String, value: String,
ns: String,
db: String,
}, },
/// The access method cannot be defined on the requested level /// The access method cannot be defined on the requested level

View file

@ -474,6 +474,57 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
))); )));
Ok(()) 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::<Claims>(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, Error> {
Role::from_str(r.as_str()).map_err(Error::IamError)
})
.collect::<Result<Vec<_>, _>>()?,
};
// 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 // Check if this is root authentication with user credentials
Claims { Claims {
id: Some(id), 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] #[tokio::test]
async fn test_token_ns() { async fn test_token_ns() {
let secret = "jwt_secret"; let secret = "jwt_secret";

View file

@ -1,6 +1,7 @@
//! How the keys are structured in the key value store //! How the keys are structured in the key value store
/// ///
/// crate::key::root::all / /// crate::key::root::all /
/// crate::key::root::ac /!ac{ac}
/// crate::key::root::hb /!hb{ts}/{nd} /// crate::key::root::hb /!hb{ts}/{nd}
/// crate::key::root::nd /!nd{nd} /// crate::key::root::nd /!nd{nd}
/// crate::key::root::ni /!ni /// crate::key::root::ni /!ni

View file

@ -28,6 +28,7 @@ pub enum Entry {
Pa(Arc<DefineParamStatement>), Pa(Arc<DefineParamStatement>),
Tb(Arc<DefineTableStatement>), Tb(Arc<DefineTableStatement>),
// Multi definitions // Multi definitions
Acs(Arc<[DefineAccessStatement]>),
Azs(Arc<[DefineAnalyzerStatement]>), Azs(Arc<[DefineAnalyzerStatement]>),
Dbs(Arc<[DefineDatabaseStatement]>), Dbs(Arc<[DefineDatabaseStatement]>),
Das(Arc<[DefineAccessStatement]>), Das(Arc<[DefineAccessStatement]>),

View file

@ -1328,6 +1328,34 @@ impl Transaction {
Ok(val) Ok(val)
} }
/// Retrieve all ROOT access method definitions.
pub async fn all_root_accesses(&mut self) -> Result<Arc<[DefineAccessStatement]>, 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<Arc<[DefineAccessStatement]>, 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. /// Retrieve all namespace definitions in a datastore.
pub async fn all_ns(&mut self) -> Result<Arc<[DefineNamespaceStatement]>, Error> { pub async fn all_ns(&mut self) -> Result<Arc<[DefineNamespaceStatement]>, Error> {
let key = crate::key::root::ns::prefix(); let key = crate::key::root::ns::prefix();
@ -1741,6 +1769,15 @@ impl Transaction {
Ok(val.into()) Ok(val.into())
} }
/// Retrieve a specific root access method definition.
pub async fn get_root_access(&mut self, ac: &str) -> Result<DefineAccessStatement, Error> {
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. /// Retrieve a specific namespace definition.
pub async fn get_ns(&mut self, ns: &str) -> Result<DefineNamespaceStatement, Error> { pub async fn get_ns(&mut self, ns: &str) -> Result<DefineNamespaceStatement, Error> {
let key = crate::key::root::ns::new(ns); let key = crate::key::root::ns::new(ns);
@ -1771,8 +1808,9 @@ impl Transaction {
ac: &str, ac: &str,
) -> Result<DefineAccessStatement, Error> { ) -> Result<DefineAccessStatement, Error> {
let key = crate::key::namespace::ac::new(ns, ac); 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(), value: ac.to_owned(),
ns: ns.to_owned(),
})?; })?;
Ok(val.into()) Ok(val.into())
} }
@ -1825,8 +1863,10 @@ impl Transaction {
ac: &str, ac: &str,
) -> Result<DefineAccessStatement, Error> { ) -> Result<DefineAccessStatement, Error> {
let key = crate::key::database::ac::new(ns, db, ac); 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(), value: ac.to_owned(),
ns: ns.to_owned(),
db: db.to_owned(),
})?; })?;
Ok(val.into()) Ok(val.into())
} }

View file

@ -60,6 +60,34 @@ impl DefineAccessStatement {
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?; opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match &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 => { Base::Ns => {
// Claim transaction // Claim transaction
let mut run = ctx.tx_lock().await; let mut run = ctx.tx_lock().await;
@ -72,6 +100,7 @@ impl DefineAccessStatement {
} else { } else {
return Err(Error::AccessNsAlreadyExists { return Err(Error::AccessNsAlreadyExists {
value: self.name.to_string(), value: self.name.to_string(),
ns: opt.ns()?.into(),
}); });
} }
} }
@ -101,6 +130,8 @@ impl DefineAccessStatement {
} else { } else {
return Err(Error::AccessDbAlreadyExists { return Err(Error::AccessDbAlreadyExists {
value: self.name.to_string(), value: self.name.to_string(),
ns: opt.ns()?.into(),
db: opt.db()?.into(),
}); });
} }
} }

View file

@ -91,6 +91,12 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("users".to_owned(), tmp.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 // Ok all good
Value::from(res).ok() Value::from(res).ok()
} }

View file

@ -27,6 +27,19 @@ impl RemoveAccessStatement {
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?; opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match &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 => { Base::Ns => {
// Claim transaction // Claim transaction
let mut run = ctx.tx_lock().await; let mut run = ctx.tx_lock().await;
@ -59,10 +72,13 @@ impl RemoveAccessStatement {
.await; .await;
match future { match future {
Err(e) if self.if_exists => match e { Err(e) if self.if_exists => match e {
Error::NaNotFound { Error::AccessRootNotFound {
.. ..
} => Ok(Value::None), } => Ok(Value::None),
Error::DaNotFound { Error::AccessNsNotFound {
..
} => Ok(Value::None),
Error::AccessDbNotFound {
.. ..
} => Ok(Value::None), } => Ok(Value::None),
e => Err(e), e => Err(e),

View file

@ -287,6 +287,14 @@ impl Parser<'_> {
} }
t!("RECORD") => { t!("RECORD") => {
self.pop_peek(); 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 { let mut ac = access_type::RecordAccess {
..Default::default() ..Default::default()
}; };

View file

@ -756,6 +756,66 @@ fn parse_define_access_jwt_key() {
res 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] #[test]
@ -1132,6 +1192,28 @@ fn parse_define_access_record() {
res 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] #[test]

View file

@ -29,6 +29,7 @@ async fn define_statement_namespace() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
namespaces: { test: 'DEFINE NAMESPACE test' }, namespaces: { test: 'DEFINE NAMESPACE test' },
users: {}, 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], vec!["{ accesses: { }, namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"],
vec!["{ namespaces: { }, users: { } }"], vec!["{ accesses: { }, namespaces: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -1428,6 +1429,48 @@ async fn permissions_checks_define_analyzer() {
assert!(res.is_ok(), "{}", res.unwrap_err()); 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] #[tokio::test]
async fn permissions_checks_define_access_ns() { async fn permissions_checks_define_access_ns() {
let scenario = HashMap::from([ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 15m, FOR SESSION 6h\" } }"], vec!["{ accesses: { }, 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: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -2105,9 +2148,9 @@ async fn define_remove_access() -> Result<(), Error> {
let mut t = Test::new(sql).await?; let mut t = Test::new(sql).await?;
t.skip_ok(1)?; t.skip_ok(1)?;
t.expect_val("None")?; 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.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")?; t.expect_val("None")?;
Ok(()) Ok(())
} }

View file

@ -12,19 +12,22 @@ async fn info_for_root() {
let sql = r#" let sql = r#"
DEFINE NAMESPACE NS; DEFINE NAMESPACE NS;
DEFINE USER user ON ROOT PASSWORD 'pass'; DEFINE USER user ON ROOT PASSWORD 'pass';
DEFINE ACCESS access ON ROOT TYPE JWT ALGORITHM HS512 KEY 'secret';
INFO FOR ROOT INFO FOR ROOT
"#; "#;
let dbs = new_ds().await.unwrap(); let dbs = new_ds().await.unwrap();
let ses = Session::owner(); let ses = Session::owner();
let mut res = dbs.execute(sql, &ses, None).await.unwrap(); 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(); let out = res.pop().unwrap().output();
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let output_regex = let output_regex = Regex::new(
Regex::new(r"\{ namespaces: \{ NS: .* \}, users: \{ user: .* \} \}").unwrap(); r"\{ accesses: \{ access: .* \}, namespaces: \{ NS: .* \}, users: \{ user: .* \} \}",
)
.unwrap();
let out_str = out.unwrap().to_string(); let out_str = out.unwrap().to_string();
assert!( assert!(
output_regex.is_match(&out_str), 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")]); 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = let check_results = [
[vec!["{ namespaces: { }, users: { } }"], vec!["{ namespaces: { }, users: { } }"]]; vec!["{ accesses: { }, namespaces: { }, users: { } }"],
vec!["{ accesses: { }, namespaces: { }, users: { } }"],
];
let test_cases = [ let test_cases = [
// Root level // Root level
@ -537,11 +542,11 @@ async fn access_info_redacted() {
// Record // Record
{ {
let sql = r#" let sql = r#"
DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret'; DEFINE ACCESS access ON DB TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret';
INFO FOR NS INFO FOR DB
"#; "#;
let dbs = new_ds().await.unwrap(); 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(); let mut res = dbs.execute(sql, &ses, None).await.unwrap();
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
@ -550,7 +555,7 @@ async fn access_info_redacted() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,
@ -610,11 +615,11 @@ async fn access_info_redacted_structure() {
// Record // Record
{ {
let sql = r#" let sql = r#"
DEFINE ACCESS access ON NS TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h; DEFINE ACCESS access ON DB TYPE RECORD WITH JWT ALGORITHM HS512 KEY 'secret' DURATION FOR TOKEN 15m, FOR SESSION 6h;
INFO FOR NS STRUCTURE INFO FOR DB STRUCTURE
"#; "#;
let dbs = new_ds().await.unwrap(); 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(); let mut res = dbs.execute(sql, &ses, None).await.unwrap();
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
@ -623,7 +628,7 @@ async fn access_info_redacted_structure() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let out_expected = 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(); let out_str = out.unwrap().to_string();
assert_eq!( assert_eq!(
out_str, out_expected, out_str, out_expected,

View file

@ -522,7 +522,7 @@ async fn should_error_when_remove_and_access_does_not_exist() -> Result<(), Erro
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
// //
let tmp = res.remove(0).result.unwrap_err(); let tmp = res.remove(0).result.unwrap_err();
assert!(matches!(tmp, Error::DaNotFound { .. }),); assert!(matches!(tmp, Error::AccessDbNotFound { .. }),);
Ok(()) 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ namespaces: { }, users: { } }"], vec!["{ accesses: { }, namespaces: { }, users: { } }"],
vec!["{ namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"], vec!["{ accesses: { }, namespaces: { NS: 'DEFINE NAMESPACE NS' }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -747,6 +747,48 @@ async fn permissions_checks_remove_analyzer() {
assert!(res.is_ok(), "{}", res.unwrap_err()); 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] #[tokio::test]
async fn permissions_checks_remove_ns_access() { async fn permissions_checks_remove_ns_access() {
let scenario = HashMap::from([ 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 // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ namespaces: { }, users: { } }"], vec!["{ accesses: { }, 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: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER DURATION FOR TOKEN 1h, FOR SESSION NONE\" } }"],
]; ];
let test_cases = [ let test_cases = [

View file

@ -233,6 +233,7 @@ async fn loose_mode_all_ok() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
namespaces: { test: 'DEFINE NAMESPACE test' }, namespaces: { test: 'DEFINE NAMESPACE test' },
users: {}, users: {},
}", }",