Allow defining JWT access at the root level (#4348)
This commit is contained in:
parent
fc154142fa
commit
e281a4e41e
14 changed files with 469 additions and 44 deletions
|
@ -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
|
||||
|
|
|
@ -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::<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
|
||||
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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,6 +28,7 @@ pub enum Entry {
|
|||
Pa(Arc<DefineParamStatement>),
|
||||
Tb(Arc<DefineTableStatement>),
|
||||
// Multi definitions
|
||||
Acs(Arc<[DefineAccessStatement]>),
|
||||
Azs(Arc<[DefineAnalyzerStatement]>),
|
||||
Dbs(Arc<[DefineDatabaseStatement]>),
|
||||
Das(Arc<[DefineAccessStatement]>),
|
||||
|
|
|
@ -1328,6 +1328,34 @@ impl Transaction {
|
|||
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.
|
||||
pub async fn all_ns(&mut self) -> Result<Arc<[DefineNamespaceStatement]>, 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<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.
|
||||
pub async fn get_ns(&mut self, ns: &str) -> Result<DefineNamespaceStatement, Error> {
|
||||
let key = crate::key::root::ns::new(ns);
|
||||
|
@ -1771,8 +1808,9 @@ impl Transaction {
|
|||
ac: &str,
|
||||
) -> Result<DefineAccessStatement, Error> {
|
||||
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<DefineAccessStatement, Error> {
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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: {},
|
||||
}",
|
||||
|
|
Loading…
Reference in a new issue